Overview
Webhooks
Section titled “Webhooks”Subscribe your endpoint to WhatSetter events. Deliveries are signed:
| Header | Value |
|---|---|
X-Whatsetter-Event |
the event type |
X-Whatsetter-Delivery |
unique delivery id (evt_…): use it to dedupe |
X-Whatsetter-Timestamp |
ISO-8601 |
X-Whatsetter-Api-Version |
2026-06-01 |
X-Whatsetter-Signature |
sha256=<hex>: HMAC-SHA256 of the raw body with your subscription secret |
Verify (Node.js):
const crypto = require('crypto');
const expected = 'sha256=' + crypto.createHmac('sha256', SECRET).update(rawBody, 'utf8').digest('hex');
const valid = crypto.timingSafeEqual(Buffer.from(header), Buffer.from(expected));
Events:
contact.qualified/contact.not_qualified: a lead was (dis)qualified by the AIbooking.created: a call was booked in chatmessage.received: a lead sent an inbound message (high volume)agent.disconnected: a WhatsApp number went offline
Delivery is at-least-once: dedupe on event_id. Payloads carry
is_test: true when triggered from a test.
Payload shape (contact.qualified):
{
"event_type": "contact.qualified",
"event_id": "evt_3f2a…",
"api_version": "2026-06-01",
"occurred_at": "2026-08-05T09:12:44.001Z",
"team_id": "…",
"campaign": { "id": "…", "slug": "…", "name": "…" },
"contact": {
"id": "…",
"lead_id": "33612345678@c.us",
"phone": "33612345678",
"name": "Marie Dupont",
"external_reference": "4471",
"classification": "QUALIFIED",
"ai_reasoning": "…",
"conversation_url": "https://app.whatsetter.com/…"
},
"is_test": false
}
Matching events to your own records. Send external_reference on
POST /campaigns/{id}/leads and read it back from
contact.external_reference — that is the intended join key. It is null
for leads created from the dashboard. If you match on the phone number
instead, note that contact.phone is digits only, without a leading
+, while POST /campaigns/{id}/leads echoes it with the +:
compare digits on both sides.

