// Integrations
Webhooks
Get told when a message arrives or a delivery status changes.
Register an https URL and we POST to it when something happens in your project. You can also manage endpoints without code under Settings → Webhooks.
Events#
| Event | Fires when |
|---|---|
message.received | An inbound message was received from a contact |
message.status_updated | A message you sent changed delivery status (sent/delivered/read/failed) |
conversation.created | A new conversation was opened |
Payload#
Every delivery has the same envelope. id is unique per delivery, so use it to ignore duplicates.
{
"id": "8f3c…",
"event": "message.received",
"occurred_at": "2026-10-05T12:00:00.000Z",
"account_id": "…",
"data": {
"conversation_id": "…",
"contact_id": "…",
"whatsapp_message_id": "wamid.…",
"content_type": "text",
"text": "Is my order on the way?"
}
}Headers: X-Wacrm-Event, X-Wacrm-Webhook-Id and X-Wacrm-Signature.
Verifying the signature#
Check every delivery before you trust it. Without this check, anyone who finds your URL can send you fake messages.
X-Wacrm-Signature: t=<unix seconds>,v1=<hex>, where v1 is HMAC-SHA256 of "<t>.<raw body>" using your endpoint's secret.
import crypto from 'node:crypto';
// X-Wacrm-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "t.rawBody">
export function verifyWacrm(rawBody, header, secret) {
const parts = Object.fromEntries(
header.split(',').map((kv) => kv.trim().split('=')),
);
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false; // replay
const expected = crypto
.createHmac('sha256', secret)
.update(`${t}.${rawBody}`)
.digest('hex');
const given = String(parts.v1 ?? '').toLowerCase();
return (
given.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected))
);
}Use the raw body
express.raw({ type: 'application/json' }) on this route.Delivery#
- Answer with a 2xx within 5 seconds. Reply first and do slow work afterwards.
- Each event is sent once, and redirects are not followed. If a delivery is missed, fetch the data from the read endpoints.
- Status updates can arrive twice or out of order, because WhatsApp sends them that way. Ignore duplicate
ids, and never let a later status be replaced by an earlier one. - After 15 failures in a row, the endpoint is switched off. Turn it back on with
PATCHor in Settings → Webhooks. - The URL must be https and must resolve to a public address. Private, loopback and cloud-metadata addresses are refused. At most 20 endpoints per project.
message.status_updatedcovers inbox and API sends, not individual campaign recipients. For those, poll the broadcast status.
/api/v1/webhooksRegister a webhook endpoint
Scope: webhooks:manage
The response includes secret once. Store it; it is never shown again.
curl -X POST https://whatsapp.esendblue.com/api/v1/webhooks \
-H "Authorization: Bearer wacrm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "url": "https://example.com/hooks/whatsapp", "events": ["message.received"] }'/api/v1/webhooksList webhook endpoints
Scope: webhooks:manage
Your endpoints. The secret is never included.
/api/v1/webhooks/{id}Read a webhook endpoint
Scope: webhooks:manage
One endpoint, including its failure_count.
/api/v1/webhooks/{id}Update or re-enable an endpoint
Scope: webhooks:manage
Change url, events or is_active. Setting is_active back to true also resets the failure count.
/api/v1/webhooks/{id}Remove an endpoint
Scope: webhooks:manage
Removes the endpoint. Deliveries already sent are not affected.