// 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#

EventFires when
message.receivedAn inbound message was received from a contact
message.status_updatedA message you sent changed delivery status (sent/delivered/read/failed)
conversation.createdA 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

Compute the signature over the bytes exactly as you received them. If your framework parses the JSON and you turn it back into a string, the keys can come out in a different order and every check fails. In Express, use 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 PATCH or 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_updated covers inbox and API sends, not individual campaign recipients. For those, poll the broadcast status.
POST/api/v1/webhooks

Register 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"] }'
GET/api/v1/webhooks

List webhook endpoints

Scope: webhooks:manage

Your endpoints. The secret is never included.

GET/api/v1/webhooks/{id}

Read a webhook endpoint

Scope: webhooks:manage

One endpoint, including its failure_count.

PATCH/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.

DELETE/api/v1/webhooks/{id}

Remove an endpoint

Scope: webhooks:manage

Removes the endpoint. Deliveries already sent are not affected.