LPLeadProof

Tally form CRM delivery

Deliver every Tally form lead to the CRM once—with proof.

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.

Protect 25 live leads free →Start Builder · $49/monthAudit this workflow
Paid plan uses secure Stripe checkout · plan assigned after verified payment · activate with the checkout email
THE FAILURE GAP

A successful automation run is not proof of delivery.

Tally 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 -> CRM
01

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.

02

Webhook retries can duplicate leads

Tally retries failed webhook deliveries, so recovery must retain the original submission ID instead of generating a new event key.

03

Submissions can include unrelated data

Files, signatures, payment fields, and every answer should not be copied when the CRM only needs approved contact fields.

Implementation

Put LeadProof in the delivery path.

Keep the tools that create and process the lead. Standardize only the fragile handoff between them.

Platform reference: Tally webhook documentationOpen sandbox instructions →
  1. Add a Tally webhook with a signing secret and point it to a public HTTPS mapping endpoint.
  2. Read the untouched JSON body, verify its base64 HMAC-SHA256 value against Tally-Signature, and accept only FORM_RESPONSE.
  3. Select the named contact fields, keep data.submissionId, place the compact job on a durable queue, and acknowledge Tally within its timeout.
  4. Let the worker send the mapped lead through LeadProof with submissionId as the Idempotency-Key, then retain the receipt ID.
POSThttps://leadproof.jessesay.chatgpt.site/api/v1/leads
import 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

What teams ask before adding LeadProof.

Why use data.submissionId for idempotency?

It identifies the original Tally submission and remains the correct business identity when Tally retries or an operator replays the webhook.

How does Tally authenticate a webhook?

When a signing secret is enabled, Tally sends a base64 SHA256 HMAC in Tally-Signature. Verify it before parsing or queueing the response.

Should the complete fields array go to the CRM?

No. Map only the specific input types and labels the destination is authorized to receive.

FREE · NO SIGNUP

See the gaps in your real workflow.

Get an explainable risk score, prioritized fixes, and the right LeadProof plan.

Protect 25 live leads free →Run the reliability auditStart Builder · $49/month