Webhooks

Webhooks are configured per WhatsApp account via the admin dashboard or REST:

curl -X POST "$PUBLIC_API_URL/v1/accounts/ACCOUNT_ID/webhooks" \
  -H "Authorization: Bearer $ADMIN_JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/wa-events",
    "events": ["*"],
    "retry_count": 5,
    "timeout_seconds": 15
  }'

Response includes the secret — store it now, it is shown only once.

Payload format

Every delivery is a POST with this body shape:

{
  "event": "message.received",
  "account_id": "acc_...",
  "timestamp": "2026-05-25T00:00:00.000Z",
  "data": {
    "message_id": "...",
    "wa_message_id": "...",
    "chat_id": "971...@s.whatsapp.net",
    "chat_type": "private",
    "direction": "incoming",
    "type": "text",
    "from": "971501234567",
    "to": null,
    "body": "Hello"
  }
}

Headers

| Header | Value |
|---|---|
| Content-Type | application/json |
| User-Agent | WhatsAppGateway-Webhook/1.0 |
| X-Webhook-Event | e.g. message.received |
| X-Webhook-Delivery-Id | unique per attempt |
| X-Webhook-Timestamp | ISO timestamp |
| X-Webhook-Signature | sha256= HMAC of raw body using webhook secret |

Signature verification

X-Webhook-Signature uses HMAC-SHA256 over the raw request body using the webhook secret.

Node:

import crypto from 'node:crypto';

function verify(rawBody, header, secret) {
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  const got = (header || '').replace(/^sha256=/, '');
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(got));
}

Python:

import hmac, hashlib

def verify(raw_body, header, secret):
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    got = (header or "").replace("sha256=", "")
    return hmac.compare_digest(expected, got)

Event types

message.received, message.sent, message.delivered, message.read, message.deleted,
chat.created, chat.updated, group.participant_added, group.participant_removed,
connection.qr, connection.connected, connection.disconnected, connection.error,
ai.reply.generated, ai.reply.sent, ai.reply.failed.

Use "*" in the events array to subscribe to all.

Retries

Failed deliveries (non-2xx, timeout, network error) are retried by a BullMQ worker with exponential backoff (5s, 25s, 2m, 10m, 50m, capped at the webhook's retry_count). Inspect history in the dashboard or via GET /api/v1/webhook-deliveries.

Common troubleshooting

| Symptom | Fix |
|---|---|
| You see deliveries marked failed with code 0 | Your endpoint is unreachable from the server — check firewall / DNS |
| code 401/403 | Your endpoint requires its own auth — most webhooks should be public |
| Signature mismatch | You verified against a stringified copy — always sign over the raw body |