Webhooks
Receiving events
How webhooks are delivered, what to do with them, and what happens when your endpoint is down.
We POST a JSON payload to your endpoint when something happens: somebody subscribed, somebody left, a campaign finished, a message bounced.
For most integrations these matter more than anything you can poll for, because they are how your own systems learn that a person’s consent changed.
Setting one up
Settings → Webhooks in the dashboard. Give us an HTTPS URL, pick the events you want, and copy the signing secret. You will need it to verify signatures.
The payload
Every event shares an envelope.
{
"id": "whe_3k9m2x7q",
"type": "contact.unsubscribed",
"created_at": "2026-03-14T09:30:00.000Z",
"data": {
"contact_id": "ct_8vq2xk4m",
"email": "ada@example.com",
"scope": "topic",
"topic_id": "tp_7xk2m9qv4w",
"topic_name": "Weekly newsletter",
"brand_id": "br_3nf8q1",
"campaign_id": "cmp_5t8w2k",
"occurred_at": "2026-03-14T09:29:58.000Z"
}
}
id— stable across retries. Dedupe on it.type— which event. See the catalogue.created_at— when we built the payload.data— the event’s own fields.data.occurred_at— when it actually happened, which can be earlier thancreated_atfor delivery events that reach us from the mail provider.
Respond fast, work later
Return a 2xx within 10 seconds. Anything else counts as a
failure.
Do the quick thing first, then the slow thing:
app.post('/webhooks/zevcampaign', async (req, res) => {
if (!verifySignature(req)) return res.sendStatus(401);
// Acknowledge, then work. A slow CRM call inside the handler is
// the usual reason an endpoint starts timing out and gets
// disabled.
res.sendStatus(200);
await queue.add('zevcampaign-event', req.body);
});
Delivery is at-least-once
If your endpoint does not answer 2xx, we retry with exponential
backoff: roughly 1, 2, 4, 8 and 16 minutes, about half an hour of
attempts.
That means you can receive the same event twice — most often when you
did the work and then timed out before answering. Dedupe on the
envelope id and make your handler safe to run twice.
Repeated failures disable the endpoint
If an endpoint fails too many times in a row, we stop sending to it and email whoever on your team manages webhooks.
Nothing else is affected. Campaigns keep sending and every event is still recorded, so your reports stay complete.
Events from while it was off are not replayed. Fix the endpoint, turn it back on, and backfill anything you need from your reports. Replaying a backlog into a system that has moved on usually causes a second incident.
Events fire once each
Some are deliberately once-only, and it is worth knowing which:
email.openedfires on a recipient’s first open. Pixels are prefetched by Gmail, scanned by security gateways and refetched on every scroll, so an event per load would be mostly machine traffic.email.clickedfires on a recipient’s first click of a given link. Later clicks still count in your reports.contact.unsubscribedfires on the first opt-out. Re-clicking an old unsubscribe link does not fire it again.
Your campaign reports always hold the full counts. Webhooks are the once-per-person signal.
Test and live
A test key never fires webhooks, deliberately. A sandbox call that reached your production endpoint would have your CRM record a subscriber who does not exist.
To test your handler, use a live key and a verified recipient address of your own.
Local development
Your endpoint has to be reachable from the public internet, so point a tunnel at it:
ngrok http 3000
# then register https://<id>.ngrok.io/webhooks/zevcampaign
The dashboard shows recent deliveries with the request, the response and the timing, which is usually enough to see what went wrong without reproducing it.
Updated at, Saturday, October 10, 2026