---
title: "WhatsApp API webhooks: a practical guide | D-API"
description: "How WhatsApp API webhooks work: setup, events, the incoming message payload, the retry policy and how to build a reliable receiver for your backend."
url: https://d-api.cloud/en/whatsapp-webhook
---

WhatsApp API guide

# WhatsApp API webhooks: how to receive messages and events

The webhook is the URL in your system that the WhatsApp API calls whenever something happens on WhatsApp: a new message, a message read, a connection that dropped. Without it, your app only talks; with it, it also listens and reacts.

By D-API engineering teamUpdated Sep 24, 20266 min read

## The role of the webhook in a WhatsApp integration

Sending a message is the easy part. The hard part is knowing what happened next: did the customer reply? Did they read it? Did the number disconnect in the middle of the night? In a [WhatsApp API](https://d-api.cloud/en), the webhook answers those questions. On every event, the API sends a `POST` to a URL you control, with a JSON body describing what happened.

This flips the integration logic. Instead of your backend asking "anything new?" every few seconds, it only works when there is work to do. For a product with hundreds of connections, that is the difference between a calm queue and thousands of useless requests per minute.

## Setup: one URL, or one URL per event

The shortest path is to set the URL when you create the connection (the `webhookUrl` field) or later, with `POST /api/v1/sessions/{sessionId}/webhook`. When you want to choose which events you receive, use the full configuration endpoint:

```
curl -X POST https://api.d-api.cloud/api/v1/sessions/my-session/webhook-config \
  -H "Authorization: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "enabled": true,
    "type": "per_event",
    "events": {
      "messages.received": { "enabled": true, "webhookUrl": "https://app.yourcompany.com/wa/k7f2q9/messages" },
      "connection.status": { "enabled": true, "webhookUrl": "https://app.yourcompany.com/wa/k7f2q9/connection" }
    }
  }'
```

The two modes serve different needs:

-   **`single`:** every enabled event goes to the same URL. Good to get started, or when a single service receives everything and distributes it internally.
-   **`per_event`:** each event has its own destination. Useful when support, monitoring and analytics are separate services.

Disabled events are not sent, so only turn on what you will actually process.

## The envelope and the events you will use first

Every event arrives in the same envelope: `event` (the event name), `sessionId` (which connection produced it), `data` (the event-specific content), `timestamp` in ISO 8601, and `traceId`, which is worth keeping in your logs to cross-reference with support. The headers are `Content-Type: application/json` and `User-Agent: Deliverify-Webhook/1.0`.

| Event | When it fires | Typical use |
| --- | --- | --- |
| `messages.received` | A message arrived (or you sent one from the phone) | Inbox, chatbot, CRM |
| `messages.sent` | A message was sent through the API | Log the sending history |
| `message.delivered` / `message.read` | Delivered to and read by the recipient | Delivery status in your UI |
| `connection.status` | Status changed to `connected`, `disconnected` or `logged_out` | Alert the customer before they notice |
| `groups_participants.join` and related | Someone joined, left or was promoted in a group | Member management |

A detail that trips up a lot of people: there is no separate edit event. An edited message arrives as `messages.received` with `type: "edited_message"`.

## The payload of an incoming message

This is the event that matters most. The `data` object carries the message `id`, the `type` (text, image, audio, list reply and others), the content in `message`, the sender in `from`, whether it came from a group in `is_group` and, for media, the link in `media_url`:

```
{
  "event": "messages.received",
  "sessionId": "customer-042",
  "timestamp": "2026-01-24T22:51:32.601Z",
  "traceId": "c17dee440402792623e3ad6d925cb000",
  "data": {
    "id": "AC9831DDA691236BA3CE4909A187B703",
    "type": "text",
    "message": "Can we move it to Friday?",
    "timestamp": 1769295092000,
    "fromMe": false,
    "is_group": false,
    "from": { "jid": "14155550123@s.whatsapp.net", "name": "Emily" },
    "from_name": "Emily",
    "media_url": null
  }
}
```

Watch `fromMe`: it is `true` when someone replied from the connected number's own phone. If your bot reacts to every message, filter this case so it does not answer itself.

## Delivery policy: what the API guarantees and what is on you

On D-API, if your endpoint fails, delivery is retried up to **7 times** with exponential backoff, at intervals of roughly 5s, 10s, 20s, 40s, 80s and 160s. Each attempt waits at most **30 seconds** for a response. **404** and **410** responses are treated as permanent failures and stop the retries. There is also a per-URL circuit breaker and event deduplication with a 5-minute window.

In practice, a short outage on your server or a slow deploy does not make you lose messages. A route deleted by mistake, returning 404, does. That is why you should monitor the error rate of your webhook endpoint like any other critical route.

## Webhook receiver checklist

1.  **Return 2xx in milliseconds.** Put the event on a queue and return `200`. Calling an LLM, a CRM or a heavy database query before responding blows past the 30 seconds and triggers a retry.
2.  **Be idempotent on `data.id`.** The API's deduplication covers short windows; a retry can arrive later. Store processed IDs and ignore repeats.
3.  **Protect the URL.** Since there is no HMAC, put a long random token in the path, check the `User-Agent` and reject any `sessionId` that does not exist in your database.
4.  **Do not return 404 during a deploy.** If the route disappears for a few seconds, retries stop. Prefer 503, which stays in the retry cycle.
5.  **Log the `traceId`.** No personal data in logs, just the event identifier.

```
app.post('/wa/:token/messages', async (req, res) => {
  if (req.params.token !== process.env.WA_WEBHOOK_TOKEN) return res.sendStatus(401)
  const { event, sessionId, data, traceId } = req.body
  await queue.add('wa-event', { event, sessionId, data, traceId }, { jobId: data.id })
  res.sendStatus(200)
})
```

Using the message ID as the `jobId` makes the queue itself drop duplicates. The real processing (replying, saving, calling the AI) happens in the worker. It is the same design used in a [WhatsApp AI chatbot](https://d-api.cloud/en/whatsapp-ai-chatbot).

## When to swap HTTP for RabbitMQ

If your volume is high or you already run a broker, D-API can publish events straight to your own RabbitMQ, configured per connection at `/sessions/{sessionId}/rabbitmq-config`. Some history sync events only exist on that channel. See the [RabbitMQ integration](https://d-api.cloud/en/integrations/rabbitmq) page.

To understand the full path, from connecting the number to the first event, read [how the WhatsApp API works](https://d-api.cloud/en/how-whatsapp-api-works). If your product will receive events from many customers at once, the per-connection design is covered in [multiple numbers](https://d-api.cloud/en/whatsapp-api-multiple-numbers) and on the [WhatsApp API for SaaS](https://d-api.cloud/en/whatsapp-api-for-saas) page.

## Frequently asked questions

### What is the difference between a webhook and polling the API?

Polling forces your system to keep asking whether anything changed, wasting requests and arriving late. With a webhook, the API calls your URL the moment the event happens, and you only process what actually changed.

### Are D-API WhatsApp webhooks signed with HMAC?

No. WhatsApp events arrive unsigned. The recommendation is to use a secret URL with a long token in the path, and to check the User-Agent and whether the sessionId belongs to one of your connections in the receiver.

### What happens if my server is down?

Delivery is retried up to 7 times with growing intervals, from about 5 seconds up to 160 seconds. If the server comes back within that window, the event arrives. A 404 or 410 response stops the retries for that URL.

### Can I send different events to different URLs?

Yes. In per\_event mode each event has its own URL, which lets you, for example, send incoming messages to your support service and connection changes to your monitoring service.

### How do I test the webhook locally?

The API needs to reach your URL over the internet. During development, use a tunnel that exposes your local port over HTTPS and register that temporary URL on your test connection. In production, switch to the permanent URL.

Try D-API's WhatsApp API3-day free trial, no credit card.[Start free trial](https://app.d-api.cloud/register?lang=en)

## Keep reading

-   [WhatsApp API guideSend images, audio and files](https://d-api.cloud/en/whatsapp-api-send-media)
-   [WhatsApp API guideWhatsApp buttons and lists](https://d-api.cloud/en/whatsapp-buttons-and-lists)
-   [WhatsApp API guideWhatsApp groups API](https://d-api.cloud/en/whatsapp-groups-api)
-   [WhatsApp API guideWhatsApp AI chatbot](https://d-api.cloud/en/whatsapp-ai-chatbot)

## Try D-API's WhatsApp API

3-day trial with full access. No credit card, no lock-in.

[Start free trial](https://app.d-api.cloud/register?lang=en)[Read the docs](https://docs.d-api.cloud/)
