---
title: "WhatsApp Wpp webhook"
description: "Messages coming in and going out, read receipts, calls, and the session lifecycle."
lang: en
canonical: https://doc.apibrasil.io/webhooks/api-whatsapp-wpp
markdown: https://doc.apibrasil.io/webhooks/api-whatsapp-wpp.md
source: apibrasil-documentation
---

# WhatsApp Wpp webhook

> Messages coming in and going out, read receipts, calls, and the session lifecycle.

Every message that arrives on your number reaches your endpoint in real time — with the text, the sender and the id. And every message you send reports when it was delivered and when it was read.

## How to receive

The four URLs are registered on the device, not on the send, and it is APIBrasil that calls them — with a JSON POST, user-agent APIBRASIL/1.0, a queue header saying which queue it came from, and 10 seconds of patience. The skeleton below routes on wook and verifies the signature before trusting the body.

**Where to register the URL and what lands on it**

| Field | What it receives | Events |
| --- | --- | --- |
| `wh_message` | Messages, read receipts, calls and conversation events. | `SEND_MESSAGE`, `RECEIVE_MESSAGE`, `MESSAGE_STATUS`, `INCOMING_CALL`, `REACTION_MESSAGE`, `REVOKED_MESSAGE`, `MESSAGE_EDIT`, `POLL_RESPONSE`, `ADDED_TO_GROUP`, `PARTICIPANTS_CHANGED`, `LIVE_LOCATION`, `NOTIFICATION_MESSAGE`, `ORDER_STATUS_UPDATE`, `INTERFACE_CHANGED` |
| `wh_connect` | The session lifecycle: connected, dropped, removed. | `STATUS_CONNECT`, `SESSION_REMOVED` |
| `wh_qrcode` | The QR code and the pairing code, for you to show on your own screen. | `QRCODE`, `CODE` |
| `wh_status` | Connection state changes reported by WhatsApp itself. | `STATUS_CONNECTION` |

The events of this channel are independent: there is no order between them.

## Events (19)

### `RECEIVE_MESSAGE` — Message received

**When it fires:** When someone sends a message to your number. It arrives with the text, the sender's name and the id — and it is the same id you use to reply with a quote.

**Fields**

| Name | Type | Meaning |
| --- | --- | --- |
| `wook` | string | The event. This is what you route on — SEND_MESSAGE, RECEIVE_MESSAGE, MESSAGE_STATUS… |
| `status` | string | The direction, not the event: SENT when the message is yours, RECEIVED when it is the contact's. |
| `type` | string | text, image, audio, ptt, video, document, location, sticker, link, vcard, list… |
| `fromMe` | boolean | True when you were the sender. |
| `id` | string | The message's id on WhatsApp. Ties it to MESSAGE_STATUS. |
| `session` | string | The session name (your device). |
| `isGroupMsg` | boolean | True when it came from a group. |
| `author` | string \| null | In a group, who actually wrote it. Null in a one-to-one chat. |
| `name` | string | The contact's display name, or an empty string. |
| `from` | string | Sender WITHOUT the suffix — the @c.us is stripped before dispatch. |
| `to` | string | Recipient, also without the suffix. |
| `content` | string | The message text. |
| `quotedMsg` | object \| string | The quoted message, or "" when there is none. |
| `quotedMsgId` | string | The quoted message's id, or "". |
| `timestamp` | number | Unix, in seconds. |
| `datetime` | string | The same instant as DD-MM-YYYY HH:mm:ss. |
| `data` | object | The raw WPPConnect message, with dozens of fields. The sensitive ones are stripped. |
| `number` | string \| null | Added on dispatch: the number, already normalized. Comes alongside fromNumber and toNumber. |

**Sample body, as it arrives**

```json
[
  {
    "wook": "RECEIVE_MESSAGE",
    "status": "RECEIVED",
    "type": "text",
    "fromMe": false,
    "id": "true_5531999999999@c.us_3EB0C15F8C2A1B9D7E4A",
    "session": "apibrasil-financeiro",
    "isGroupMsg": false,
    "author": null,
    "name": "Maria Souza",
    "from": "5531999999999",
    "to": "5531988888888",
    "content": "Good afternoon! Can you reissue the invoice?",
    "quotedMsg": "",
    "quotedMsgId": "",
    "timestamp": 1756400930,
    "datetime": "28-08-2026 14:28:50",
    "number": "5531999999999",
    "fromNumber": "5531999999999",
    "toNumber": "5531988888888",
    "data": {
      "…": "the raw WPPConnect message"
    }
  }
]
```

### `SEND_MESSAGE` — Message sent

**When it fires:** When your number sends a message — including from the handset, not just through the API. It is how you keep your system in sync with what was answered on the phone.

**Fields**

| Name | Type | Meaning |
| --- | --- | --- |
| `wook` | string | The event. This is what you route on — SEND_MESSAGE, RECEIVE_MESSAGE, MESSAGE_STATUS… |
| `status` | string | The direction, not the event: SENT when the message is yours, RECEIVED when it is the contact's. |
| `type` | string | text, image, audio, ptt, video, document, location, sticker, link, vcard, list… |
| `fromMe` | boolean | True when you were the sender. |
| `id` | string | The message's id on WhatsApp. Ties it to MESSAGE_STATUS. |
| `session` | string | The session name (your device). |
| `isGroupMsg` | boolean | True when it came from a group. |
| `author` | string \| null | In a group, who actually wrote it. Null in a one-to-one chat. |
| `name` | string | The contact's display name, or an empty string. |
| `from` | string | Sender WITHOUT the suffix — the @c.us is stripped before dispatch. |
| `to` | string | Recipient, also without the suffix. |
| `content` | string | The message text. |
| `quotedMsg` | object \| string | The quoted message, or "" when there is none. |
| `quotedMsgId` | string | The quoted message's id, or "". |
| `timestamp` | number | Unix, in seconds. |
| `datetime` | string | The same instant as DD-MM-YYYY HH:mm:ss. |
| `data` | object | The raw WPPConnect message, with dozens of fields. The sensitive ones are stripped. |
| `number` | string \| null | Added on dispatch: the number, already normalized. Comes alongside fromNumber and toNumber. |

**Sample body, as it arrives**

```json
[
  {
    "wook": "SEND_MESSAGE",
    "status": "SENT",
    "type": "text",
    "fromMe": true,
    "id": "true_5531999999999@c.us_9F1D3A7C2E5B8046",
    "session": "apibrasil-financeiro",
    "isGroupMsg": false,
    "author": null,
    "name": "",
    "from": "5531988888888",
    "to": "5531999999999",
    "content": "Of course! I'll generate it for you right away.",
    "quotedMsg": "",
    "quotedMsgId": "",
    "timestamp": 1756401002,
    "datetime": "28-08-2026 14:30:02",
    "number": "5531999999999",
    "fromNumber": "5531988888888",
    "toNumber": "5531999999999",
    "data": {
      "…": "the raw WPPConnect message"
    }
  }
]
```

### `MESSAGE_STATUS` — Delivered, read, or failed

**When it fires:** On every state change of a message of YOURS. The id is the same as SEND_MESSAGE's — that is what joins them. The states are CLOCK, SENT, RECEIVED, READ and PLAYED on the happy path; FAILED, EXPIRED, INACTIVE, CONTENT_GONE, CONTENT_TOO_BIG, CONTENT_UNUPLOADABLE and MD_DOWNGRADE when it goes wrong.

**Fields**

| Name | Type | Meaning |
| --- | --- | --- |
| `wook` | string | Always "MESSAGE_STATUS" in this notification. |
| `status` | string | The state. RECEIVED means the handset got it; READ means the contact opened it; PLAYED only applies to audio. |
| `type` | string | The type of the message whose state changed. |
| `id` | string | The message id. The SAME as in the send notification. |
| `from` | string | Without the WhatsApp suffix. |
| `to` | string | Without the WhatsApp suffix. |
| `session` | string | The session name. |
| `dateTime` | string | With a CAPITAL T — unlike the datetime in message notifications. |
| `data` | object | The raw ack, with the original numeric code. |

**Sample body, as it arrives**

```json
[
  {
    "wook": "MESSAGE_STATUS",
    "status": "READ",
    "type": "text",
    "id": "true_5531999999999@c.us_9F1D3A7C2E5B8046",
    "from": "5531988888888",
    "to": "5531999999999",
    "session": "apibrasil-financeiro",
    "dateTime": "28-08-2026 14:31:15",
    "data": {
      "ack": 3
    }
  }
]
```

### `QRCODE` — QR code for pairing

**When it fires:** When the session needs to be authenticated. It arrives on wh_qrcode, with the image already in base64, ready to drop into an <img src>.

**Fields**

| Name | Type | Meaning |
| --- | --- | --- |
| `wook` | string | Always "QRCODE". |
| `result` | number | 200 in connection notifications. |
| `session` | string | The session name. |
| `state` | string | "QRCODE_RECEIVED". |
| `status` | string | "awaitReadQrCode" — waiting for someone to scan it. |
| `qrcode` | string | A base64 PNG data URL. It is the whole image, not the code's text. |
| `attempts` | number | Which generation attempt this is. |
| `urlCode` | string | The code's contents, so you can render the image yourself. |

**Sample body, as it arrives**

```json
[
  {
    "wook": "QRCODE",
    "result": 200,
    "session": "apibrasil-financeiro",
    "state": "QRCODE_RECEIVED",
    "status": "awaitReadQrCode",
    "qrcode": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAOEAAADhCAMAAA…",
    "attempts": 1,
    "urlCode": "2@Xk9…"
  }
]
```

### `STATUS_CONNECT` — The session changed state

**When it fires:** On every change in the session lifecycle: INITIALIZING, CONNECTED, browserClose, autocloseCalled, desconnectedMobile. It arrives on wh_connect — this is how you find out the number dropped before the customer complains.

**Fields**

| Name | Type | Meaning |
| --- | --- | --- |
| `wook` | string | Always "STATUS_CONNECT". |
| `result` | number | 200. |
| `session` | string | The session name. |
| `state` | string | The new state. This is the field you read to decide whether you need to reconnect. |
| `status` | string | The client's status at the time of the notification. |
| `number` | string | The session's number, or empty if it has not paired yet. |
| `message` | object | A plain-English explanation of what happened. |
| `body` | array \| object | Extra context for the event. |

**Sample body, as it arrives**

```json
[
  {
    "wook": "STATUS_CONNECT",
    "result": 200,
    "session": "apibrasil-financeiro",
    "state": "browserClose",
    "status": "browserClose",
    "number": "",
    "message": {
      "message": "If the browser is closed this parameter is returned."
    },
    "body": []
  }
]
```

### `INCOMING_CALL` — Incoming call

**When it fires:** When someone calls your number. It arrives on wh_message, and the format differs from a message's — it carries phone with the WhatsApp suffix, not from.

**Fields**

| Name | Type | Meaning |
| --- | --- | --- |
| `wook` | string | Always "INCOMING_CALL". |
| `id` | string | The call's id. |
| `phone` | string | Who called, WITH the @c.us suffix — this notification does not strip it, unlike the message ones. |
| `offer_time` | string | When the call was offered, as DD-MM-YYYY HH:mm:ss. |
| `isVideo` | boolean | Video call or voice call. |
| `isGroup` | boolean | Group call. |
| `participants` | array \| null | Who is on the call, when it is a group call. |
| `session` | string | The session name. |

**Sample body, as it arrives**

```json
[
  {
    "wook": "INCOMING_CALL",
    "id": "3EB0A1B2C3D4E5F60718",
    "phone": "5531999999999@c.us",
    "offer_time": "28-08-2026 14:35:11",
    "isVideo": false,
    "isGroup": false,
    "participants": null,
    "session": "apibrasil-financeiro",
    "data": {
      "…": "the raw WPPConnect call"
    }
  }
]
```

### `SESSION_REMOVED` — The session was removed

**When it fires:** When the session ceases to exist in the service. It is the channel's terminal notification: no further event will come from this session until it is created again.

**Fields**

| Name | Type | Meaning |
| --- | --- | --- |
| `wook` | string | Always "SESSION_REMOVED". |
| `session` | string | The session that went away. |
| `state` | string | "REMOVED". |
| `status` | string | "REMOVED". |
| `reason` | string | The recorded reason for the removal. |
| `removed_at` | string | ISO 8601, in UTC. |

**Sample body, as it arrives**

```json
[
  {
    "wook": "SESSION_REMOVED",
    "result": 200,
    "session": "apibrasil-financeiro",
    "state": "REMOVED",
    "status": "REMOVED",
    "reason": "removed",
    "removed_at": "2026-08-28T17:40:03.512Z"
  }
]
```

### `STATUS_CONNECTION` — Connection state

**When it fires:** When WhatsApp itself reports a state change — CONNECTED, DISCONNECTED, TIMEOUT, CONFLICT. It is the only event that goes out on wh_status, and the payload is the channel's smallest: state and status carry the SAME value.

**Fields**

| Name | Type | Meaning |
| --- | --- | --- |
| `wook` | string | Always "STATUS_CONNECTION". |
| `result` | number | 200. |
| `session` | string | The session name. |
| `state` | string | The state reported by WhatsApp. |
| `status` | string | The SAME value as state — the emitter fills both from the same variable. |

**Sample body, as it arrives**

```json
[
  {
    "wook": "STATUS_CONNECTION",
    "result": 200,
    "session": "apibrasil-financeiro",
    "state": "CONNECTED",
    "status": "CONNECTED"
  }
]
```

### `CODE` — Pairing code

**When it fires:** When you pair by CODE instead of QR code. It goes out on the SAME URL as the QR code (wh_qrcode), so your receiver has to tell the two apart by wook.

**Fields**

| Name | Type | Meaning |
| --- | --- | --- |
| `wook` | string | Always "CODE". |
| `result` | number | 200. |
| `session` | string | The session name. |
| `state` | string | "CODE_RECEIVED". |
| `status` | string | "awaitReadCode" — waiting for someone to type it. |
| `code` | string | The code to type on the handset. |
| `number` | string | The number that requested pairing. |

**Sample body, as it arrives**

```json
[
  {
    "wook": "CODE",
    "result": 200,
    "session": "apibrasil-financeiro",
    "state": "CODE_RECEIVED",
    "status": "awaitReadCode",
    "code": "1A2B-3C4D",
    "number": "5531988888888"
  }
]
```

### `REACTION_MESSAGE` — Reaction to a message

**When it fires:** When someone reacts to a message with an emoji. It arrives on wh_message — the same URL as messages — and it is not a message: it has neither content nor type.

**Fields**

| Name | Type | Meaning |
| --- | --- | --- |
| `wook` | string | Always "REACTION_MESSAGE" in this notification. |
| `session` | string | The session name (your device). |
| `reaction` | object | The reaction: the emoji, the id of the message reacted to, and who reacted. |
| `data` | object | The SAME object, nested again under reaction. data.reaction equals reaction. |
| `number` | string \| null | Added on dispatch, alongside fromNumber, toNumber, lid and phoneNumberByLid. Null when the event has no number. |

**Sample body, as it arrives**

```json
[
  {
    "wook": "REACTION_MESSAGE",
    "session": "apibrasil-financeiro",
    "reaction": {
      "…": "WPPConnect's reaction object"
    },
    "data": {
      "reaction": {
        "…": "the same object, repeated"
      }
    },
    "number": null
  }
]
```

### `REVOKED_MESSAGE` — Message deleted

**When it fires:** When someone deletes a message for everyone. It is the notification that lets you mirror "this message was deleted" on your side — without it, your copy keeps showing what WhatsApp no longer does.

**Fields**

| Name | Type | Meaning |
| --- | --- | --- |
| `wook` | string | Always "REVOKED_MESSAGE" in this notification. |
| `session` | string | The session name (your device). |
| `revoked` | object | The revoked message and who revoked it. |
| `data` | object | The SAME object, nested again under revoked. data.revoked equals revoked. |
| `number` | string \| null | Added on dispatch, alongside fromNumber, toNumber, lid and phoneNumberByLid. Null when the event has no number. |

**Sample body, as it arrives**

```json
[
  {
    "wook": "REVOKED_MESSAGE",
    "session": "apibrasil-financeiro",
    "revoked": {
      "…": "WPPConnect's revoked object"
    },
    "data": {
      "revoked": {
        "…": "the same object, repeated"
      }
    },
    "number": null
  }
]
```

### `MESSAGE_EDIT` — Message edited

**When it fires:** When an already-sent message is edited. The text you stored from RECEIVE_MESSAGE is no longer the current text.

**Fields**

| Name | Type | Meaning |
| --- | --- | --- |
| `wook` | string | Always "MESSAGE_EDIT" in this notification. |
| `session` | string | The session name (your device). |
| `editedMessage` | object | The message after the edit. |
| `data` | object | The SAME object, nested again under editedMessage. data.editedMessage equals editedMessage. |
| `number` | string \| null | Added on dispatch, alongside fromNumber, toNumber, lid and phoneNumberByLid. Null when the event has no number. |

**Sample body, as it arrives**

```json
[
  {
    "wook": "MESSAGE_EDIT",
    "session": "apibrasil-financeiro",
    "editedMessage": {
      "…": "WPPConnect's editedMessage object"
    },
    "data": {
      "editedMessage": {
        "…": "the same object, repeated"
      }
    },
    "number": null
  }
]
```

### `POLL_RESPONSE` — Poll response

**When it fires:** When someone votes in a poll you sent.

**Fields**

| Name | Type | Meaning |
| --- | --- | --- |
| `wook` | string | Always "POLL_RESPONSE" in this notification. |
| `session` | string | The session name (your device). |
| `poll` | object | The vote: which option, in which poll, from whom. |
| `data` | object | The SAME object, nested again under poll. data.poll equals poll. |
| `number` | string \| null | Added on dispatch, alongside fromNumber, toNumber, lid and phoneNumberByLid. Null when the event has no number. |

**Sample body, as it arrives**

```json
[
  {
    "wook": "POLL_RESPONSE",
    "session": "apibrasil-financeiro",
    "poll": {
      "…": "WPPConnect's poll object"
    },
    "data": {
      "poll": {
        "…": "the same object, repeated"
      }
    },
    "number": null
  }
]
```

### `LIVE_LOCATION` — Live location

**When it fires:** While someone shares their location in real time. It repeats for as long as the sharing lasts — the only event in this channel that fires in a series on its own.

**Fields**

| Name | Type | Meaning |
| --- | --- | --- |
| `wook` | string | Always "LIVE_LOCATION" in this notification. |
| `session` | string | The session name (your device). |
| `liveLocation` | object | The current position of whoever is sharing. |
| `data` | object | The SAME object, nested again under liveLocation. data.liveLocation equals liveLocation. |
| `number` | string \| null | Added on dispatch, alongside fromNumber, toNumber, lid and phoneNumberByLid. Null when the event has no number. |

**Sample body, as it arrives**

```json
[
  {
    "wook": "LIVE_LOCATION",
    "session": "apibrasil-financeiro",
    "liveLocation": {
      "…": "WPPConnect's liveLocation object"
    },
    "data": {
      "liveLocation": {
        "…": "the same object, repeated"
      }
    },
    "number": null
  }
]
```

### `ADDED_TO_GROUP` — You were added to a group

**When it fires:** When your number joins a group. The key is chat, not group — it is the conversation object, not the group's.

**Fields**

| Name | Type | Meaning |
| --- | --- | --- |
| `wook` | string | Always "ADDED_TO_GROUP" in this notification. |
| `session` | string | The session name (your device). |
| `chat` | object | The conversation for the group you joined. |
| `data` | object | The SAME object, nested again under chat. data.chat equals chat. |
| `number` | string \| null | Added on dispatch, alongside fromNumber, toNumber, lid and phoneNumberByLid. Null when the event has no number. |

**Sample body, as it arrives**

```json
[
  {
    "wook": "ADDED_TO_GROUP",
    "session": "apibrasil-financeiro",
    "chat": {
      "…": "WPPConnect's chat object"
    },
    "data": {
      "chat": {
        "…": "the same object, repeated"
      }
    },
    "number": null
  }
]
```

### `PARTICIPANTS_CHANGED` — Group participants changed

**When it fires:** When someone joins, leaves, is promoted to admin or demoted, in any group you are part of.

**Fields**

| Name | Type | Meaning |
| --- | --- | --- |
| `wook` | string | Always "PARTICIPANTS_CHANGED" in this notification. |
| `session` | string | The session name (your device). |
| `participantChanged` | object | Who changed, in which group, and what the action was. |
| `data` | object | The SAME object, nested again under participantChanged. data.participantChanged equals participantChanged. |
| `number` | string \| null | Added on dispatch, alongside fromNumber, toNumber, lid and phoneNumberByLid. Null when the event has no number. |

**Sample body, as it arrives**

```json
[
  {
    "wook": "PARTICIPANTS_CHANGED",
    "session": "apibrasil-financeiro",
    "participantChanged": {
      "…": "WPPConnect's participantChanged object"
    },
    "data": {
      "participantChanged": {
        "…": "the same object, repeated"
      }
    },
    "number": null
  }
]
```

### `ORDER_STATUS_UPDATE` — Order status

**When it fires:** When an order from the WhatsApp Business catalog changes state. Only meaningful on accounts with a catalog set up.

**Fields**

| Name | Type | Meaning |
| --- | --- | --- |
| `wook` | string | Always "ORDER_STATUS_UPDATE" in this notification. |
| `session` | string | The session name (your device). |
| `order` | object | The order and its new state. |
| `data` | object | The SAME object, nested again under order. data.order equals order. |
| `number` | string \| null | Added on dispatch, alongside fromNumber, toNumber, lid and phoneNumberByLid. Null when the event has no number. |

**Sample body, as it arrives**

```json
[
  {
    "wook": "ORDER_STATUS_UPDATE",
    "session": "apibrasil-financeiro",
    "order": {
      "…": "WPPConnect's order object"
    },
    "data": {
      "order": {
        "…": "the same object, repeated"
      }
    },
    "number": null
  }
]
```

### `NOTIFICATION_MESSAGE` — WhatsApp notification

**When it fires:** System notices that appear inside the conversation — a number change, joining a group, a description edit. They are nobody's messages.

**Fields**

| Name | Type | Meaning |
| --- | --- | --- |
| `wook` | string | Always "NOTIFICATION_MESSAGE" in this notification. |
| `session` | string | The session name (your device). |
| `notification` | object | The notice issued by WhatsApp. |
| `data` | object | The SAME object, nested again under notification. data.notification equals notification. |
| `number` | string \| null | Added on dispatch, alongside fromNumber, toNumber, lid and phoneNumberByLid. Null when the event has no number. |

**Sample body, as it arrives**

```json
[
  {
    "wook": "NOTIFICATION_MESSAGE",
    "session": "apibrasil-financeiro",
    "notification": {
      "…": "WPPConnect's notification object"
    },
    "data": {
      "notification": {
        "…": "the same object, repeated"
      }
    },
    "number": null
  }
]
```

### `INTERFACE_CHANGED` — Interface changed

**When it fires:** When the WhatsApp Web interface switches mode. It is diagnostic: if it changed on its own, something happened to the session. The envelope differs from the other room events.

**Fields**

| Name | Type | Meaning |
| --- | --- | --- |
| `wook` | string | Always "INTERFACE_CHANGED". |
| `result` | number | 200. |
| `session` | string | The session name. |
| `message` | object | A sentence assembled by the service describing the change, under message.message. |
| `body` | object | The change object: displayInfo, mode and info. |

**Sample body, as it arrives**

```json
[
  {
    "wook": "INTERFACE_CHANGED",
    "result": 200,
    "session": "apibrasil-financeiro",
    "message": {
      "message": "apibrasil-financeiro is SYNCING mode MAIN whatsapp, info CONNECTED"
    },
    "body": {
      "displayInfo": "SYNCING",
      "mode": "MAIN",
      "info": "CONNECTED"
    }
  }
]
```

## What usually bites

- **There are four URLs, and an event will not switch doors** — wh_message, wh_connect, wh_qrcode and wh_status are separate fields on the device. Registering only wh_message and waiting for the QR code there does not work — the QR code goes out through wh_qrcode, and nothing warns you that you never registered it.
- **wook says the event, status says the direction** — They are two fields and easy to mix up. On a message, wook is SEND_MESSAGE or RECEIVE_MESSAGE and status is SENT or RECEIVED — redundant. On MESSAGE_STATUS, though, wook is always MESSAGE_STATUS and status carries the real state (SENT, RECEIVED, READ, PLAYED, FAILED). Routing on status breaks at the first read receipt.
- **A message type outside the known list is DISCARDED** — The builder has a case for 14 types (text, image, sticker, audio, ptt, video, location, document, link, vcard, multi_vcard, order, list, list_response). Anything else falls into the default branch, which uses the TYPE IN UPPERCASE as the wook — and that name does not exist in the event registry, so the dispatch is aborted without an error. If a new WhatsApp type never reaches your endpoint, this is why.
- **datetime on messages, dateTime on the receipt** — The message notification carries datetime, all lowercase. The MESSAGE_STATUS one carries dateTime, with a capital T. Same format, different names, in the same channel — reading datetime on a MESSAGE_STATUS returns undefined.
- **The notification can arrive twice, and that is by design** — Besides your device's URL, the service has a GLOBAL webhook. When both are configured, every event is posted to both destinations. If they point at the same place you get everything twice — deduplicate on id plus wook.
- **Verify the signature before trusting the body** — When the session has a key, the POST comes with x-webhook-signature: sha256=<hmac>, computed over the RAW body using the device's sessionkey. Comparing it requires the body before JSON parsing — after JSON.parse and re-serializing, the hash no longer matches.

## Good practices

- **Verify the signature over the RAW body** — x-webhook-signature is sha256= plus the HMAC-SHA256 of the body with the device's sessionkey. Compute it over the bytes you received: if you JSON.parse and re-serialize, one extra space already breaks the hash.
- **You have 10 seconds** — That is the POST timeout. Respond 200 and do the work afterwards. With the service's retry queue enabled, a failure is retried up to 3 times every 5 seconds; without it, the notification is lost silently.
- **Route on wook, never on status** — status is SENT both on a message of yours and on a MESSAGE_STATUS for a delivered message. Two different things with the same value — only wook tells them apart.
- **Deduplicate on id + wook** — The global webhook and the device's webhook both fire. If they point at the same URL, every event arrives twice.

## Sample receivers

**Node**

```node
import crypto from "node:crypto"

// The RAW body is required to verify the signature.
app.post("/webhook/whatsapp", express.raw({ type: "application/json" }), (req, res) => {
  const signature = req.get("x-webhook-signature")
  const expected =
    "sha256=" + crypto.createHmac("sha256", SESSION_KEY).update(req.body).digest("hex")

  if (signature && signature !== expected) return res.sendStatus(401)

  // Respond before processing: you have 10 seconds.
  res.sendStatus(200)

  const notification = JSON.parse(req.body.toString("utf8"))

  switch (notification.wook) {
    case "RECEIVE_MESSAGE":
      console.log(`${notification.name || notification.from}: ${notification.content}`)
      break
    case "MESSAGE_STATUS":
      // Careful: dateTime with a capital T in this notification.
      console.log(`${notification.id} is now ${notification.status}`)
      break
    case "STATUS_CONNECT":
      if (notification.state === "browserClose") alertSessionDropped(notification.session)
      break
  }
})
```

**Python**

```python
import hashlib
import hmac

@app.post("/webhook/whatsapp")
def webhook_whatsapp():
    body = request.get_data()  # raw bytes, so the signature matches
    signature = request.headers.get("x-webhook-signature")
    expected = "sha256=" + hmac.new(SESSION_KEY.encode(), body, hashlib.sha256).hexdigest()

    if signature and not hmac.compare_digest(signature, expected):
        return "", 401

    notification = request.get_json(force=True)

    if notification["wook"] == "RECEIVE_MESSAGE":
        print(f"{notification.get('name') or notification['from']}: {notification['content']}")
    elif notification["wook"] == "MESSAGE_STATUS":
        # Careful: dateTime with a capital T in this notification.
        print(f"{notification['id']} is now {notification['status']}")
    elif notification["wook"] == "STATUS_CONNECT" and notification["state"] == "browserClose":
        alert_session_dropped(notification["session"])

    return "", 200
```

**PHP**

```php
<?php

// The RAW body is required to verify the signature.
$body = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? null;
$expected = 'sha256=' . hash_hmac('sha256', $body, SESSION_KEY);

if ($signature && !hash_equals($expected, $signature)) {
    http_response_code(401);
    exit;
}

http_response_code(200);

$notification = json_decode($body, true);

switch ($notification['wook']) {
    case 'RECEIVE_MESSAGE':
        error_log(($notification['name'] ?: $notification['from']) . ': ' . $notification['content']);
        break;
    case 'MESSAGE_STATUS':
        // Careful: dateTime with a capital T in this notification.
        error_log("{$notification['id']} is now {$notification['status']}");
        break;
    case 'STATUS_CONNECT':
        if ($notification['state'] === 'browserClose') {
            alertSessionDropped($notification['session']);
        }
        break;
}
```

## See also

- Webhooks: [Webhooks](https://doc.apibrasil.io/webhooks.md)
- Documentation index: https://doc.apibrasil.io/llms.txt
- HTML version of this page: https://doc.apibrasil.io/webhooks/api-whatsapp-wpp
