Create a free digital business card, then share it by link, QR code, or NFC tap.

Lead webhooks

When lead webhooks are available for your account and delivery is enabled, Zapped sends each new lead to your saved HTTPS destination as a signed JSON POST. This page is the receiver reference. Destination setup and live delivery become available only after your account is enrolled and the service is enabled.

HeaderDescription
Content-Typeapplication/json
webhook-idThe event ID. The same event keeps the same ID on every retry and replay.
webhook-timestampUnix seconds when this attempt was signed.
webhook-signaturev1, followed by the base64 HMAC SHA-256 signature. This follows the Standard Webhooks format. For 24 hours after you replace your signing secret the header holds two signatures separated by a space, one made with the new secret and one with the previous secret.

api_documentation.example

{
    "id": "evt_0123456789abcdef0123456789abcdef",
    "type": "lead.created",
    "schema_version": 1,
    "occurred_at": "2026-09-24T14:05:12Z",
    "data": {
        "lead": {
            "id": "98765",
            "duplicate_of_lead_id": null,
            "capture_preset": "contact",
            "intent": "contact",
            "source_channel": "card",
            "name": "Alex Morgan",
            "email": "[email protected]",
            "phone": null,
            "company": "Brightside Studio",
            "message": "Please call me back."
        },
        "card": {
            "id": "123",
            "block_id": "456",
            "name": "Maya Chen",
            "url_alias": "maya-chen"
        },
        "consent": {
            "notice_language": "en",
            "privacy_notice_version": 1,
            "marketing_consent_at": null,
            "marketing_consent_version": null,
            "marketing_consent_source": null
        },
        "custom_fields": {
            "schema_version": 1,
            "template_key": "contact",
            "intent": "contact",
            "form_headline": "Get in touch",
            "answers": [
                {
                    "question_id": "q1",
                    "question_label": "Best time to call",
                    "type": "single_choice",
                    "selected_options": [
                        {
                            "option_id": "o2",
                            "option_label": "Afternoon"
                        }
                    ]
                }
            ]
        }
    }
}
FieldTypeDescription
idstringEvent ID, evt_ followed by 32 hex characters. Also sent as the webhook-id header. Use it to ignore repeats.
typestringlead.created for real leads, webhook.test for test events.
schema_versionintegerCurrently 1.
occurred_atstring (UTC)When the lead was captured, in UTC. Use it to order leads, because deliveries can arrive out of order.
data.leadobjectThe lead: id, duplicate_of_lead_id, capture_preset, intent, source_channel, name, email, phone, company and message. Empty values are null.
data.cardobjectThe card that captured it: id, block_id, name and url_alias at capture time.
data.consentobjectPrivacy notice language and version, and marketing consent time, version and source when given.
data.custom_fieldsobject or nullThe form snapshot: schema_version, template_key, intent, form_headline and answers. Each answer has question_id, question_label, type, then value or selected_options. Null when the form had no custom questions.
  1. Remove whsec_ from your signing secret and base64 decode the rest. That is the key.
  2. Compute HMAC SHA-256 over webhook-id.webhook-timestamp.body, using the raw body exactly as received.
  3. Base64 encode it, prefix v1, and compare it in constant time with each space separated signature in the header. Accept the request when any one matches.
  4. Reject timestamps more than five minutes from your clock, and ignore an event ID you already processed.

Any Standard Webhooks library can do this for you with the same secret, including headers with more than one signature.

Replacing your signing secret. Leads already waiting to be sent are kept and are signed with the new secret. For the next 24 hours every request also carries a second signature made with the previous secret, so a receiver that still has the old secret keeps accepting requests while you update it. After 24 hours only the new secret signs. Replacing the destination URL, disabling the destination or revoking it does not keep waiting leads.

PHP

$secret  = base64_decode(substr($signingSecret, 6)); // remove "whsec_"
$id      = $_SERVER['HTTP_WEBHOOK_ID'];
$time    = $_SERVER['HTTP_WEBHOOK_TIMESTAMP'];
$body    = file_get_contents('php://input');
$expect  = 'v1,' . base64_encode(hash_hmac('sha256', "$id.$time.$body", $secret, true));
$given   = explode(' ', $_SERVER['HTTP_WEBHOOK_SIGNATURE']);

if (abs(time() - (int) $time) > 300 || !in_array(true, array_map(fn($s) => hash_equals($expect, $s), $given), true)) {
    http_response_code(400);
    exit;
}
http_response_code(204); // then process the lead

Node.js

import crypto from 'node:crypto';

// Use the raw request body exactly as received.
function verify(rawBody, headers, signingSecret) {
  const key = Buffer.from(signingSecret.slice('whsec_'.length), 'base64');
  const id = headers['webhook-id'];
  const time = headers['webhook-timestamp'];
  if (Math.abs(Date.now() / 1000 - Number(time)) > 300) return false;
  const expected = 'v1,' + crypto.createHmac('sha256', key).update(`${id}.${time}.${rawBody}`).digest('base64');
  return headers['webhook-signature'].split(' ').some((s) =>
    s.length === expected.length && crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected)));
}
Your responseWhat Zapped does
2xxDelivered. Nothing more is sent for this event.
408, 429, 5xxRetried automatically. A Retry-After header is respected. On a 429 or 5xx it also pauses every other delivery to your destination until that time, for at most one hour.
410 GoneNot retried. Zapped also pauses your destination and tells the account owner. New leads keep waiting until the owner resumes delivery on Lead webhooks, for up to 24 hours from each lead. A lead that waited longer can be replayed from Lead history.
Timeout or network errorRetried automatically. Connecting may take up to 2 seconds and the whole request up to 5 seconds.
Other 4xxNot retried. The delivery is marked failed and can be replayed from Lead history.
Redirect or TLS problemNot retried. Redirects are never followed, so give the final HTTPS address.

Automatic retries wait 1 minute, 5 minutes, 15 minutes, 1 hour, 2 hours, 6 hours and 12 hours, with at most 8 attempts within 24 hours of the lead. Reply quickly with a 2xx and process the lead afterwards. Bodies are at most 128 KB.

Leads can arrive in a different order than they were captured, for example after a retry or during a burst of submissions. Use occurred_at when order matters, and the event ID to ignore repeats.

A test from the Test history tab arrives signed the same way, with "type": "webhook.test" and "synthetic": true. Its data has the same structure as a real lead, with invented values and every identifier set to null, so you can build your field mapping from it. No real lead is created. Handle it like a lead, or acknowledge and ignore it.

{
    "id": "evt_fedcba9876543210fedcba9876543210",
    "type": "webhook.test",
    "schema_version": 1,
    "synthetic": true,
    "occurred_at": "2026-09-24T14:10:00Z",
    "data": {
        "lead": {
            "id": null,
            "duplicate_of_lead_id": null,
            "capture_preset": "enquiry",
            "intent": "enquiry",
            "source_channel": "vcard",
            "name": "Example Contact",
            "email": "[email protected]",
            "phone": "+1 555 0100",
            "company": "Example Company",
            "message": "Invented sample data for a webhook connection test."
        },
        "card": {
            "id": null,
            "block_id": null,
            "name": "Example Card",
            "url_alias": "example-card"
        },
        "consent": {
            "notice_language": "english",
            "privacy_notice_version": 1,
            "marketing_consent_at": "2026-09-24T14:10:00Z",
            "marketing_consent_version": 1,
            "marketing_consent_source": "lead_form"
        },
        "custom_fields": {
            "schema_version": 1,
            "template_key": "project_enquiry",
            "intent": "enquiry",
            "form_headline": "Start a project",
            "answers": [
                {
                    "question_id": "q_0a1b2c3d4e5f",
                    "question_label": "Project budget",
                    "type": "single_choice",
                    "selected_options": [
                        {
                            "option_id": "o_0a1b2c3d4e5f",
                            "option_label": "Under 5000"
                        }
                    ]
                },
                {
                    "question_id": "q_1a2b3c4d5e6f",
                    "question_label": "Preferred start date",
                    "type": "short_text",
                    "value": "Next month"
                }
            ]
        }
    }
}