Webhooks
Be told when a message arrives, instead of reading on a schedule to find out.
PUT Set a webhook
/webhook · needs accounts · every plan
One webhook per API key. Today the only event is message.received: a new message from someone else on WhatsApp. What you send yourself, and what you read, is never an event. An account's capabilities.webhooks says whether its channel can.
- Set.
PUT /webhookwithurl,eventsand, optionally,channelscreates it or replaces it. The address must be https and must not resolve to a private address. The first answer carries the signing secret, once; add?rotate=trueto issue a new one, which stops the old at once. - Channels. Name the channels you want, from WhatsApp. Left out, a new webhook takes every channel that is on for you, and an existing one keeps the channels it has — a webhook set before there was a choice is Gmail's, so it keeps hearing about email and nothing it did not ask for. A channel that is off for you is a
400. - Check.
GET /webhook(needs read) says where it points, which channels it listens to, when it last delivered, whether the receiver is answering (stateisactiveorfailing) and how many events failed in the last 24 hours.404if there is none. - Try.
POST /webhook/testsends a signedpingnow; any 2xx counts as delivered. - Delete.
DELETE /webhook,204. Stops the events and drops what was still owed.
curl -X PUT https://messengeragent.dev/api/v1/webhook \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://app.yourcompany.com/hooks/messages", "events": ["message.received"], "channels": ["whatsapp"]}'
# 200 — the secret is in this answer only. Store it.
{
"url": "https://app.yourcompany.com/hooks/messages",
"events": ["message.received"],
"channels": ["whatsapp"],
"createdAt": "2026-10-12T08:30:00+00:00",
"secret": "whsec_…"
}
# Try the receiver: sends a signed "ping" now and says what it answered
curl -X POST https://messengeragent.dev/api/v1/webhook/test -H "Authorization: Bearer $API_KEY"
# { "delivered": true, "statusCode": 200, "error": null }
# Is it healthy?
curl https://messengeragent.dev/api/v1/webhook -H "Authorization: Bearer $API_KEY"
# { "url": "…", "events": ["message.received"], "channels": ["whatsapp"], "createdAt": "…", "state": "active",
# "lastDelivery": { "at": "…", "succeeded": true, "statusCode": 200, "lastDeliveredAt": "…" },
# "failuresLast24Hours": 0 }The event
Each event is a JSON POST to your address, signed with X-AgentMessenger-Signature: sha256=<HMAC-SHA256 of the raw body, keyed by the secret>. Delivery is at least once: a retry carries the same event id, so de-duplicate on it. Answer any 2xx to acknowledge. Failures are retried after 30 seconds, 2 minutes, 10 minutes, 1 hour, then every 6 hours, for 24 hours. A receiver that has failed for longer is marked failing and tried once an hour until it answers. The event's channel and accountId say where it came from; the message has the id, conversationId and text the messages endpoint returns.
- WhatsApp. Announced as the message arrives, in any chat of the account, a group included. Attachments are not listed;
typesays what it is, spelled as the messages endpoint spells it.
{
"id": "evt_0199…",
"type": "message.received",
"createdAt": "2026-11-03T09:00:03Z",
"accountId": "01a115a8-…",
"channel": "whatsapp",
"message": {
"id": "3EB0A1B2C3D4",
"conversationId": "15551230001@c.us",
"direction": "inbound",
"fromMe": false,
"type": "text",
"subject": null,
"text": "Hi! Do you have the blue hoodie in size M?",
"at": "2026-11-03T09:00:00Z",
"sender": { "name": "Aigerim K." },
"to": [], "cc": [], "attachments": []
}
}Verify the signature
Compute the HMAC over the raw request body, before you parse it, and compare it to the header in constant time.
// Node: check the signature against the raw body — before you parse it
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex')
const ok = header && header.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(header), Buffer.from(expected))