# Webhooks

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

Source: https://edesy.in/docs/waflow/api/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](#verifying-signatures)) |
| `Content-Type` | `application/json` |

**Body:**
```json
{
  "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`

```json
{
  "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`

```json
{
  "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:

```json
{
  "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`

```json
{
  "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:**
```javascript
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.
