Sign inGet an API key
API reference

Webhooks

Signed event delivery for messages, contacts and campaigns.

Webhooks push events to your server as they happen, so you don’t need to poll. Add an endpoint under Webhooks in the dashboard, choose the events you want, and copy the signing secret — it’s shown once.

Events

EventSent when
message.receivedA customer sends you a message
message.sentWhatsApp accepted an outbound message
message.deliveredDelivered to the customer’s device
message.readThe customer read it
message.failedDelivery failed
contact.createdA new contact was created
campaign.completedA campaign finished sending

Registered events: message.received, message.sent, message.delivered, message.read, message.failed, contact.created, campaign.completed. You can also send a ping test from the dashboard.

Request format

We send an HTTPS POST with a JSON body and these headers:

HeaderValue
X-Waba-EventEvent name, e.g. message.received
X-Waba-TimestampUnix time (seconds) when the request was signed
X-Waba-Signaturesha256= + hex HMAC (see below)
User-AgentWABA-Panel-Webhook/1.0
{
  "event": "message.received",
  "created_at": "2026-10-07T10:15:00+00:00",
  "data": {
    "id": 101, "wamid": "wamid.HBgM…", "from": "14155550100",
    "type": "text", "body": "Hi!", "name": "Jane"
  }
}

Verify the signature

The signature is HMAC-SHA256(secret, timestamp + "." + rawBody). Use the raw request body — not re-serialised JSON — and a constant-time comparison. Reject requests whose timestamp is more than five minutes old to prevent replays.

const crypto = require('crypto');

app.post('/waba-webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const ts  = req.headers['x-waba-timestamp'];
  const sig = req.headers['x-waba-signature'];
  const expected = 'sha256=' + crypto.createHmac('sha256', process.env.WABA_SECRET)
    .update(ts + '.' + req.body.toString('utf8')).digest('hex');

  const ok = sig && sig.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
  if (!ok) return res.sendStatus(401);
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.sendStatus(400);

  const event = JSON.parse(req.body);
  // …handle event, then reply fast…
  res.sendStatus(200);
});
<?php
$ts   = $_SERVER['HTTP_X_WABA_TIMESTAMP'];
$sig  = $_SERVER['HTTP_X_WABA_SIGNATURE'];
$body = file_get_contents('php://input');
$expected = 'sha256=' . hash_hmac('sha256', $ts . '.' . $body, getenv('WABA_SECRET'));
if (!hash_equals($expected, $sig) || abs(time() - (int)$ts) > 300) { http_response_code(401); exit; }

Delivery & retries