Webhooks
Webhooks send an HTTP POST to a URL you choose when something happens in your demos. Use them to add leads to your CRM, post to Slack, or start any automation.
Add an endpoint
Go to Settings → Webhooks, paste an https:// URL (for example a Zapier Catch Hook URL) and pick the events it should receive. You can add up to 5 endpoints. Press Send test to post a sample event so you can map its fields before a real one arrives.
Events
| Event | When |
|---|---|
lead.created |
A viewer submits a demo's lead form. |
demo.completed |
A viewer reaches the end of a demo (once per viewing session). |
demo.cta_clicked |
A viewer clicks the demo's call-to-action (once per viewing session). |
demo.published |
You publish a demo, or publish it again. |
Each event is sent once. If your endpoint is down or answers with an error, that event is not retried. The last attempt's result shows in Settings, and every lead stays on the demo's Leads tab.
Payload
{
"id": "evt_V1StGXR8_Z5j",
"type": "lead.created",
"createdAt": "2026-10-04T12:00:00.000Z",
"test": false,
"data": {
"demo": { "id": "…", "title": "Onboarding tour", "slug": "abcdefghjk", "url": "https://demomyproduct.com/d/abcdefghjk" },
"lead": { "id": "…", "name": "Ada Lovelace", "email": "ada@example.com", "company": "Analytical Engines", "extra": {}, "referrer": "https://example.com", "createdAt": "…" }
}
}
demo.completedanddemo.cta_clickedcarrydemo,viewerId,sessionId,referrerandlead(the viewer's lead if they already filled in the form, otherwisenull).demo.publishedcarriesdemo,versionandfirstPublish.testistruefor events sent with Send test.
Verify the signature
Every request has a DMP-Signature header like t=1759579200,v1=5257a8…. v1 is the hex HMAC-SHA256 of t, a dot and the raw request body, keyed with the endpoint's signing secret (Settings → Webhooks → Reveal).
import crypto from 'node:crypto'
function verify(rawBody, header, secret) {
const { t, v1 } = Object.fromEntries(header.split(',').map((p) => p.split('=')))
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex')
const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300
return fresh && expected.length === v1.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1))
}
Requests also carry DMP-Event (the event type) and DMP-Delivery (the event id, the same for every endpoint, so you can ignore duplicates).