Form success is not CRM proof
A respondent can finish the form while a later CRM request times out, rejects a field, or disappears between systems.
Tally form CRM delivery
Verify signed Tally form responses, map only the approved contact fields, and protect every CRM write with the original submission ID and a delivery receipt.
Paid plan uses secure Stripe checkout · plan assigned after verified payment · activate with the checkout emailTally can retry a form webhook when the endpoint does not return a successful response within ten seconds, but webhook receipt still does not prove the downstream CRM stored the lead. A production bridge should verify Tally-Signature, keep data.submissionId stable across recovery, and forward only the fields the destination is authorized to receive.
Tally FORM_RESPONSE -> signed mapping handler -> durable queue -> LeadProof -> CRMA respondent can finish the form while a later CRM request times out, rejects a field, or disappears between systems.
Tally retries failed webhook deliveries, so recovery must retain the original submission ID instead of generating a new event key.
Files, signatures, payment fields, and every answer should not be copied when the CRM only needs approved contact fields.
Implementation
Keep the tools that create and process the lead. Standardize only the fragile handoff between them.
Platform reference: Tally webhook documentation ↗Open sandbox instructions →https://leadproof.jessesay.chatgpt.site/api/v1/leadsimport crypto from "node:crypto";
app.use(
"/tally/form-response",
express.raw({ type: "application/json" })
);
app.post("/tally/form-response", async (req, res) => {
const signature = req.headers["tally-signature"];
const secret = process.env.TALLY_WEBHOOK_SECRET;
if (!signature || typeof signature !== "string" || !secret) {
return res.sendStatus(401);
}
const expected = crypto
.createHmac("sha256", secret)
.update(req.body)
.digest();
const supplied = Buffer.from(signature, "base64");
const authentic =
supplied.length === expected.length &&
crypto.timingSafeEqual(supplied, expected);
if (!authentic) return res.sendStatus(401);
const event = JSON.parse(req.body.toString("utf-8"));
if (event.eventType !== "FORM_RESPONSE") return res.sendStatus(204);
const fields = event.data?.fields ?? [];
const valueFor = (type, label) =>
fields.find(
(field) =>
field.type === type &&
(!label || field.label?.toLowerCase() === label)
)?.value;
const lead = {
name: valueFor("INPUT_TEXT", "name"),
email: valueFor("INPUT_EMAIL"),
phone: valueFor("INPUT_PHONE_NUMBER")
};
const submissionId = event.data?.submissionId;
if (submissionId && lead.name && lead.email) {
await deliveryQueue.send({ submissionId, lead });
}
return res.sendStatus(204);
});
export async function deliverTallyLead({ submissionId, lead }) {
const response = await fetch(
"https://leadproof.jessesay.chatgpt.site/api/v1/leads",
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.LEADPROOF_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": submissionId
},
body: JSON.stringify({
destination: process.env.CRM_WEBHOOK_URL,
...lead,
source: "tally_form_response"
})
}
);
if (!response.ok) throw new Error("Lead delivery failed");
const delivery = await response.json();
await persistReceipt(submissionId, delivery.receipt?.id);
}Questions
It identifies the original Tally submission and remains the correct business identity when Tally retries or an operator replays the webhook.
When a signing secret is enabled, Tally sends a base64 SHA256 HMAC in Tally-Signature. Verify it before parsing or queueing the response.
No. Map only the specific input types and labels the destination is authorized to receive.
Get an explainable risk score, prioritized fixes, and the right LeadProof plan.