Webhooks

Receive inbound WhatsApp messages, delivery status updates, and campaign events in real time via signed webhooks.

Webhooks

Webhooks let WAFlow push events to your server in real time — inbound customer messages, delivery/read receipts, and campaign completion — so you don't have to poll.

Subscribe

  1. In the WAFlow dashboard, go to Settings → Webhooks.
  2. Add an endpoint: your HTTPS URL and the events you want to receive.
  3. Copy the generated signing secret (whsec_…) — you'll use it to verify payloads.

WAFlow delivers each event as an HTTP POST to your URL, with retries on failure.

Request format

Each delivery is a JSON POST with these headers:

Header Description
X-Webhook-Event The event name, e.g. message.received
X-Webhook-Signature HMAC‑SHA256 signature of the raw body (see Verifying)
Content-Type application/json

Body:

{
  "event": "message.received",
  "timestamp": "2026-07-21T09:00:00.000Z",
  "data": { }
}

Respond with a 2xx status quickly (within 5 seconds). Non‑2xx responses are retried.

Event catalog

Event Fires when
message.received An inbound customer message arrives
message.sent An outbound message was accepted by WhatsApp
message.delivered An outbound message was delivered
message.read An outbound message was read
message.failed An outbound message failed
campaign.completed A campaign finished (with final stats)
bot.conversation.started / completed / timeout / handed_over / abandoned Bot conversation lifecycle
sla.first_response.at_risk / breached, sla.resolution.at_risk / breached SLA monitoring

message.received

{
  "event": "message.received",
  "timestamp": "2026-07-21T09:00:00.000Z",
  "data": {
    "message_id": "wamid.HBgMOTE5...",
    "conversation_id": 2304,
    "from": "+919547531359",
    "contact_id": 88,
    "contact_name": "Raj Sharma",
    "type": "text",
    "text": "Hi, is my order shipped?",
    "timestamp": "2026-07-21T09:00:00.000Z"
  }
}

Note: WhatsApp/WAFlow may retry deliveries, so message.received is at‑least‑once. De‑duplicate on message_id.


message.sent / delivered / read / failed

{
  "event": "message.delivered",
  "timestamp": "2026-07-21T09:00:03.000Z",
  "data": {
    "message_id": "wamid.HBgMOTE5...",
    "recipient": "+919812345678",
    "status": "delivered",
    "campaign_id": 128,
    "timestamp": "2026-07-21T09:00:03.000Z"
  }
}

For message.failed, data.error carries the reason:

{
  "event": "message.failed",
  "data": {
    "message_id": "wamid.HBgMOTE5...",
    "recipient": "+919812345678",
    "status": "failed",
    "error": { "code": 131049, "title": "Re-engagement message", "message": "..." }
  }
}

campaign_id is null for messages sent outside a campaign (e.g. via POST /api/v1/messages).


campaign.completed

{
  "event": "campaign.completed",
  "timestamp": "2026-07-21T09:05:00.000Z",
  "data": {
    "campaign_id": 128,
    "campaignName": "July Promo",
    "successCount": 480,
    "failureCount": 20,
    "totalContacts": 500
  }
}

Verifying signatures

WAFlow signs every payload with HMAC‑SHA256 using your endpoint's signing secret, sent in the X-Webhook-Signature header (hex). Always verify it before trusting a request, using the raw request body.

Node.js / Express Example:

import express from 'express';
import crypto from 'crypto';

const app = express();
const SECRET = process.env.WAFLOW_WEBHOOK_SECRET; // whsec_...

// Capture the raw body so the signature can be verified.
app.use('/webhooks/waflow', express.raw({ type: 'application/json' }));

app.post('/webhooks/waflow', (req, res) => {
  const signature = req.get('X-Webhook-Signature');
  const expected = crypto.createHmac('sha256', SECRET).update(req.body).digest('hex');

  const ok =
    signature &&
    signature.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));

  if (!ok) return res.status(401).send('Invalid signature');

  const event = JSON.parse(req.body.toString());
  console.log(event.event, event.data);

  res.sendStatus(200); // acknowledge quickly
});

Note: Compute the HMAC over the exact bytes you received — parsing and re‑serializing the JSON first will change the bytes and break verification. Use a constant‑time comparison (crypto.timingSafeEqual).

Retries & reliability

  • Failed deliveries (non‑2xx, timeouts, or connection errors) are retried.
  • Design your handler to be idempotent — key on data.message_id (messaging events) or data.campaign_id (campaign events).
  • Acknowledge with 2xx as soon as you've stored the event; do heavy processing asynchronously so you stay under the timeout.