Skip to main content

Lead webhook

Owners and admins with a verified email. Organization settings → Integrations → Lead webhook sends every new contact from a contact form to one address, such as a Zapier, Make or n8n trigger or your own server. Contacts also stay in Analytics → Contacts. With Also send recipient link opens and completions, it also reports recipient links.

The address​

The address must use HTTPS on the default port and be reachable from the internet. Addresses that resolve to a private network are refused, and redirects are not followed, so enter the final address. Unticking Send new contacts pauses sending without removing the address; contacts submitted meanwhile are not sent later, and retries already waiting resume when you tick it again.

Send test event posts a sample contact once, without retries, and shows the result under Recent deliveries.

What is sent​

Each new contact is posted as JSON:

{
"id": "6c0a4f8e-…",
"event": "lead.created",
"createdAt": "2026-10-25T12:00:00Z",
"organization": { "id": "…", "name": "Northstar Studio" },
"demo": { "id": "…", "title": "Approve a release", "url": "https://demo-creator.com/demo/…" },
"lead": {
"id": "…",
"name": "Grace Hopper",
"email": "grace@example.com",
"screen": "start",
"language": "en"
}
}

screen is start or end, depending on which screen's form was sent, and language is the demo language the viewer had selected, or empty. Test events have "event": "webhook.test", "demo": null and a sample contact. Each request also carries the headers X-Demo-Creator-Event, X-Demo-Creator-Delivery (the same value as id) and X-Demo-Creator-Signature.

Recipient link events, recipient.opened and recipient.completed, are sent once per link and carry a recipient object instead of lead:

{
"event": "recipient.opened",
"recipient": {
"id": "…",
"name": "Grace Hopper",
"email": "grace@example.com",
"firstViewedAt": "2026-10-25T12:00:00Z",
"views": 1,
"completedAt": null
}
}

Check the signature​

X-Demo-Creator-Signature looks like t=1792931200,v1=5f2b…. Compute an HMAC-SHA256 of the timestamp, a period and the raw request body, keyed with the Signing secret, and compare it with v1 in constant time. Reject requests whose timestamp is more than five minutes old.

import { createHmac, timingSafeEqual } from 'node:crypto';

function verify(rawBody, header, secret) {
const { t, v1 } = Object.fromEntries(header.split(',').map((part) => part.split('=')));
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300;
return (
fresh &&
v1?.length === expected.length &&
timingSafeEqual(Buffer.from(v1), Buffer.from(expected))
);
}

Rotate secret signs requests with a new secret immediately, so update your receiver right after.

Delivery and retries​

A response with a 2xx status counts as delivered. Any other status, no response within 10 seconds, or no connection is retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours; after that the delivery shows as failed. A retry after a lost response can repeat a contact, so use id to ignore duplicates.

A contact whose demo is moved to trash before delivery is not sent, and Remove webhook cancels deliveries that are still waiting. The 20 latest deliveries are listed; finished deliveries are removed after 30 days.