The full payload exceeds CRM need
Paperform's documented payload can contain IP, charge, and every answer even when the destination only needs a few contact fields.
Paperform CRM delivery
Authenticate Paperform at a private adapter, keep its submission ID as the delivery identity, and forward only approved contact fields before the webhook deadline.
Paid plan uses secure Stripe checkout · plan assigned after verified payment · activate with the checkout emailPaperform posts a JSON submission payload that can include every form answer, the submitter's IP address, and payment information. Its webhook requests have a maximum duration of about 10 seconds. Sending that payload directly to a CRM creates unnecessary data exposure, while waiting synchronously for every downstream system makes timeouts and manual resends harder to reconcile.
Paperform new submission -> authenticated adapter -> durable queue -> LeadProof -> CRMPaperform's documented payload can contain IP, charge, and every answer even when the destination only needs a few contact fields.
Paperform recommends returning success early for complex processing because webhook requests have a maximum duration of around 10 seconds.
Paperform assigns each submission a unique submission_id, so the same business event can remain duplicate-safe across tests, replays, and worker retries.
Implementation
Keep the tools that create and process the lead. Standardize only the fragile handoff between them.
Platform reference: Paperform webhook documentation ↗Open sandbox instructions →https://leadproof.jessesay.chatgpt.site/api/v1/leadsimport 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 answer(payload, customKey) {
const item = Array.isArray(payload.data)
? payload.data.find((field) => field?.custom_key === customKey)
: undefined;
return typeof item?.value === "string" ? item.value.trim() : undefined;
}
app.post(
"/paperform/submission",
(req, res, next) => {
if (
!sameSecret(
req.get("X-Paperform-Webhook-Token"),
process.env.PAPERFORM_WEBHOOK_TOKEN
)
) {
return res.sendStatus(401);
}
return next();
},
express.json({ limit: "64kb" }),
async (req, res) => {
const submissionId = req.body?.submission_id;
const lead = {
name: answer(req.body, "full_name"),
email: answer(req.body, "email"),
phone: answer(req.body, "phone")
};
if (typeof submissionId !== "string" || !lead.name || !lead.email) {
await recordRejectedEntry({
source: "paperform",
reason: "missing_approved_fields"
});
return res.sendStatus(204);
}
await deliveryQueue.send({ submissionId, lead });
return res.sendStatus(202);
}
);
export async function deliverPaperformLead({ 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: "paperform"
})
}
);
if (!response.ok) throw new Error("Lead delivery failed");
const delivery = await response.json();
await persistReceipt(submissionId, delivery.receipt?.id);
}Questions
Paperform documents custom_key as the custom pre-fill key for an answer. Stable approved keys are safer to map than editable display titles.
No. This lead workflow deliberately excludes both, along with unrelated answers, unless a separate documented purpose explicitly requires them.
Paperform recommends returning a success response early for endpoints with complex or extended logic. Queueing first preserves durability without spending the webhook deadline on downstream work.
Get an explainable risk score, prioritized fixes, and the right LeadProof plan.