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 team6 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, 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.

EventWhen it firesTypical use
messages.receivedA message arrived (or you sent one from the phone)Inbox, chatbot, CRM
messages.sentA message was sent through the APILog the sending history
message.delivered / message.readDelivered to and read by the recipientDelivery status in your UI
connection.statusStatus changed to connected, disconnected or logged_outAlert the customer before they notice
groups_participants.join and relatedSomeone joined, left or was promoted in a groupMember 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": "[email protected]", "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.

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 page.

To understand the full path, from connecting the number to the first event, read how the WhatsApp API works. If your product will receive events from many customers at once, the per-connection design is covered in multiple numbers and on the 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 API

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