LPLeadProof

Paperform CRM delivery

Deliver Paperform submissions without leaking the whole payload.

Authenticate Paperform at a private adapter, keep its submission ID as the delivery identity, and forward only approved contact fields before the webhook deadline.

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.

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

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.

02

Slow downstream work risks timeout

Paperform recommends returning success early for complex processing because webhook requests have a maximum duration of around 10 seconds.

03

Resends need one delivery identity

Paperform assigns each submission a unique submission_id, so the same business event can remain duplicate-safe across tests, replays, and worker retries.

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: Paperform webhook documentationOpen sandbox instructions →
  1. Create a New Submission webhook in After Submission -> Integrations & Webhooks and add a long random X-Paperform-Webhook-Token custom header.
  2. Verify the token before JSON parsing, require submission_id, and read only the approved custom_key values for name, email, and phone.
  3. Write the compact mapped event to a durable queue, then return 202 without waiting for LeadProof or the CRM.
  4. Use submission_id as the LeadProof Idempotency-Key on every queued attempt and retain the LeadProof receipt ID for operator verification.
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 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

What teams ask before adding LeadProof.

Why use custom_key instead of question titles?

Paperform documents custom_key as the custom pre-fill key for an answer. Stable approved keys are safer to map than editable display titles.

Should the adapter forward ip_address or charge?

No. This lead workflow deliberately excludes both, along with unrelated answers, unless a separate documented purpose explicitly requires them.

Why return 202 after queueing?

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.

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