Form success is not CRM acceptance
The visitor can finish the form while a later CRM request times out, rejects the data, or disappears between systems.
Framer form CRM delivery
Verify Framer's signed webhook, preserve the submission ID through retries, and deliver only approved form fields to the CRM with duplicate protection.
Paid plan uses secure Stripe checkout · plan assigned after verified payment · activate with the checkout emailFramer retries a form webhook up to five times when the destination does not return a direct 2xx response, but successful webhook receipt still does not prove the CRM stored the lead. A production bridge should verify Framer-Signature against the raw body plus Framer-Webhook-Submission-Id, acknowledge the form promptly, and keep that submission ID stable through downstream recovery.
Framer form -> signed mapping handler -> durable queue -> LeadProof -> CRMThe visitor can finish the form while a later CRM request times out, rejects the data, or disappears between systems.
A non-2xx response can trigger up to five retries, so every attempt needs the original submission ID as one business event.
Framer sends input names as JSON keys; only the fields explicitly authorized for the destination should enter the delivery job.
Implementation
Keep the tools that create and process the lead. Standardize only the fragile handoff between them.
Platform reference: Framer form webhook documentation ↗Open sandbox instructions →https://leadproof.jessesay.chatgpt.site/api/v1/leadsimport crypto from "node:crypto";
app.use(
"/framer/form",
express.raw({ type: "application/json" })
);
app.post("/framer/form", async (req, res) => {
const signature = req.headers["framer-signature"];
const submissionId = req.headers["framer-webhook-submission-id"];
const secret = process.env.FRAMER_WEBHOOK_SECRET;
if (
typeof signature !== "string" ||
typeof submissionId !== "string" ||
!secret ||
!/^sha256=[0-9a-f]{64}$/i.test(signature)
) {
return res.sendStatus(401);
}
const expected = crypto
.createHmac("sha256", secret)
.update(req.body)
.update(submissionId)
.digest();
const supplied = Buffer.from(signature.slice(7), "hex");
const authentic =
supplied.length === expected.length &&
crypto.timingSafeEqual(supplied, expected);
if (!authentic) return res.sendStatus(401);
const form = JSON.parse(req.body.toString("utf-8"));
const lead = {
name: form.name,
email: form.email,
phone: form.phone
};
if (lead.name && lead.email) {
await deliveryQueue.send({ submissionId, lead });
}
return res.sendStatus(204);
});
export async function deliverFramerLead({ 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: "framer_form"
})
}
);
if (!response.ok) throw new Error("Lead delivery failed");
const delivery = await response.json();
await persistReceipt(submissionId, delivery.receipt?.id);
}Questions
Framer defines it as the unique identity for the form submission, so retries should keep that value instead of creating a new event key.
Framer documents an HMAC-SHA256 over the raw form payload followed by the submission ID, formatted as sha256 plus the hexadecimal digest.
No. Framer documents that webhook redirects are not followed; the configured destination must return a direct 2xx response.
Get an explainable risk score, prioritized fixes, and the right LeadProof plan.