LPLeadProof

Fillout form CRM delivery

Turn each Fillout submission into a duplicate-safe CRM receipt.

Authenticate the Fillout webhook, preserve its Submission ID, allowlist approved contact fields, and deliver the mapped lead through LeadProof with a verifiable 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.

Fillout can POST form responses to a webhook and gives every response a Submission ID, but a successful webhook test or accepted callback is not proof that the final CRM stored the lead. A production adapter should authenticate the inbound request, discard unapproved fields, keep the Submission ID stable through recovery, and record the downstream LeadProof receipt.

Fillout form -> secret-checked adapter -> durable queue -> LeadProof -> CRM
01

Webhook receipt is not CRM proof

The form can reach the adapter while a later CRM timeout, rate limit, or validation error still loses the sales handoff.

02

Flexible questions need an allowlist

Fillout submissions can contain changing question names and types; the adapter should forward only the contact fields approved for the destination.

03

Recovery needs stable identity

Using a new timestamp during a retry turns one Fillout submission into a second delivery event instead of recovering the original.

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: Fillout webhook documentationOpen sandbox instructions →
  1. Create a Fillout POST webhook to an HTTPS adapter and add a long random shared token with Fillout's custom-header option.
  2. Verify the token before parsing business data, require submissionId, and map only approved name, email, and phone questions.
  3. Enqueue the compact mapped lead under the Fillout Submission ID and acknowledge the webhook without waiting for CRM work.
  4. Send the queued lead through LeadProof with submissionId as the Idempotency-Key, then retain the LeadProof receipt ID with the source event or operator log.
POSThttps://leadproof.jessesay.chatgpt.site/api/v1/leads
import crypto from "node:crypto";

function sameSecret(supplied, expected) {
  if (!supplied || !expected) return false;
  const left = Buffer.from(supplied);
  const right = Buffer.from(expected);
  return left.length === right.length && crypto.timingSafeEqual(left, right);
}

function answerByName(questions, names) {
  const allowed = new Set(names.map((name) => name.toLowerCase()));
  const match = questions.find(
    (question) =>
      typeof question?.name === "string" &&
      allowed.has(question.name.toLowerCase()) &&
      typeof question.value === "string"
  );
  return match?.value;
}

app.post(
  "/fillout/submission",
  express.json({ limit: "64kb" }),
  async (req, res) => {
    if (
      !sameSecret(
        req.get("X-Fillout-Token"),
        process.env.FILLOUT_WEBHOOK_TOKEN
      )
    ) {
      return res.sendStatus(401);
    }

    const submissionId = req.body?.submissionId;
    const questions = Array.isArray(req.body?.questions)
      ? req.body.questions
      : [];
    if (typeof submissionId !== "string" || !submissionId) {
      return res.sendStatus(400);
    }

    const lead = {
      name: answerByName(questions, ["name", "full name"]),
      email: answerByName(questions, ["email", "work email"]),
      phone: answerByName(questions, ["phone", "phone number"])
    };
    if (!lead.name || !lead.email) return res.sendStatus(202);

    await deliveryQueue.send({ submissionId, lead });
    return res.sendStatus(202);
  }
);

export async function deliverFilloutLead({ 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: "fillout_form"
      })
    }
  );

  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 the Fillout Submission ID?

Fillout documents a unique Submission ID for each response and includes submissionId in the webhook-compatible response shape, making it the durable identity for retries and replay.

Can Fillout call LeadProof directly?

Fillout supports custom webhook bodies and headers, but a small adapter is safer when question names vary because it can authenticate the callback, allowlist fields, queue work, and retain the receipt.

Does LeadProof keep the complete Fillout response?

No. Send only the approved mapped lead fields. LeadProof production delivery stores operational metadata and a payload fingerprint rather than intentionally retaining the complete payload.

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