---
title: "WhatsApp WhatsMeow webhook"
description: "Messages, read receipts, calls, groups and connection — with the event name included, and a filter to choose what arrives."
lang: en
canonical: https://doc.apibrasil.io/webhooks/api-whatsapp-whatsmeow
markdown: https://doc.apibrasil.io/webhooks/api-whatsapp-whatsmeow.md
source: apibrasil-documentation
---

# WhatsApp WhatsMeow webhook

> Messages, read receipts, calls, groups and connection — with the event name included, and a filter to choose what arrives.

It is the only WhatsApp channel where the notification says which event it is, and the only one where you choose what you receive: subscribe filters at the source, so your endpoint is only woken by what matters.

## How to receive

The URL is registered on the instance's CONNECT and it is evolution-go that calls it, directly — with no APIBrasil relay in between. The body has five fields and the event name comes in event, in PascalCase. You choose what you receive in subscribe, which accepts 16 categories besides ALL, the default.

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

| Field | What it receives | Events |
| --- | --- | --- |
| `webhook_wh_qr_code` | EVERY event. The device has four URL fields, but this channel uses only the first one filled in — see the caveat about the order. | every event of the channel |

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

## Events (18)

### `Message` — Message received

**When it fires:** When any message arrives. It is also the event for undecryptable messages (UndecryptableMessage), which arrive under the same name. Enabled by the MESSAGE, GROUP and NEWSLETTER categories — the group and channel ones reuse this same event.

**Fields**

| Name | Type | Meaning |
| --- | --- | --- |
| `event` | string | "Message" — in PascalCase, NOT the uppercase used in subscribe. |
| `data` | object | whatsmeow's events.Message, raw: Info with the sender, id and timestamp, and Message with the content. |
| `instanceToken` | string | The instance token, INSIDE the body. This is what you check the origin against — there is no signature in this channel. |
| `instanceId` | string | The instance user's id. |
| `instanceName` | string | The instance name. |

**Sample body, as it arrives**

```json
[
  {
    "event": "Message",
    "data": {
      "…": "whatsmeow's events.Message, with Info and Message"
    },
    "instanceToken": "<instance token>",
    "instanceId": "<user id>",
    "instanceName": "apibrasil-whatsmeow"
  }
]
```

### `QRCode` — QR code for pairing

**When it fires:** When the instance needs to be authenticated. It is the only event whose data is assembled by the service rather than being whatsmeow's raw struct — which is why the fields below are exact. Enabled by the QRCODE category.

**Fields**

| Name | Type | Meaning |
| --- | --- | --- |
| `event` | string | "QRCode", in PascalCase. |
| `data.qrcode` | string | The image in base64, ready to display. |
| `data.code` | string | The code's contents, so you can render the image yourself. |
| `data.count` | number | Which attempt this is. The QR expires and is regenerated several times. |
| `data.maxCount` | number | How many attempts before giving up. On reaching the cap, the connection fails. |
| `instanceToken` | string | The instance token, in the body. |
| `instanceName` | string | The instance name. |

**Sample body, as it arrives**

```json
[
  {
    "event": "QRCode",
    "data": {
      "qrcode": "data:image/png;base64,…",
      "code": "2@Xk9…",
      "count": 1,
      "maxCount": 6
    },
    "instanceToken": "<instance token>",
    "instanceId": "<user id>",
    "instanceName": "apibrasil-whatsmeow"
  }
]
```

### `Receipt` — Delivered, read or played

**When it fires:** When a message of yours changes state on the recipient's handset. Enabled by the READ_RECEIPT category in subscribe.

**Fields**

| Name | Type | Meaning |
| --- | --- | --- |
| `event` | string | "Receipt" — in PascalCase, NOT the uppercase used in subscribe. |
| `data` | object | whatsmeow's events.Receipt: the message ids, the receipt type and who acknowledged. |
| `instanceToken` | string | The instance token, INSIDE the body. This is what you check the origin against — there is no signature in this channel. |
| `instanceId` | string | The instance user's id. |
| `instanceName` | string | The instance name. |

**Sample body, as it arrives**

```json
[
  {
    "event": "Receipt",
    "data": {
      "…": "whatsmeow's events.Receipt: the message ids, the receipt type and who acknowledged."
    },
    "instanceToken": "<instance token>",
    "instanceId": "<user id>",
    "instanceName": "apibrasil-whatsmeow"
  }
]
```

### `Connected` — The instance connected

**When it fires:** When the session comes up. The PushNameSetting event also arrives under this same name. Enabled by the CONNECTION category in subscribe.

**Fields**

| Name | Type | Meaning |
| --- | --- | --- |
| `event` | string | "Connected" — in PascalCase, NOT the uppercase used in subscribe. |
| `data` | object | whatsmeow's connection event. Empty or nearly so — what matters is that it arrived. |
| `instanceToken` | string | The instance token, INSIDE the body. This is what you check the origin against — there is no signature in this channel. |
| `instanceId` | string | The instance user's id. |
| `instanceName` | string | The instance name. |

**Sample body, as it arrives**

```json
[
  {
    "event": "Connected",
    "data": {
      "…": "whatsmeow's connection event. Empty or nearly so — what matters is that it arrived."
    },
    "instanceToken": "<instance token>",
    "instanceId": "<user id>",
    "instanceName": "apibrasil-whatsmeow"
  }
]
```

### `Disconnected` — The instance dropped

**When it fires:** When the connection is lost. This is the notification that triggers your reconnect. Enabled by the CONNECTION category in subscribe.

**Fields**

| Name | Type | Meaning |
| --- | --- | --- |
| `event` | string | "Disconnected" — in PascalCase, NOT the uppercase used in subscribe. |
| `data` | object | whatsmeow's events.Disconnected. |
| `instanceToken` | string | The instance token, INSIDE the body. This is what you check the origin against — there is no signature in this channel. |
| `instanceId` | string | The instance user's id. |
| `instanceName` | string | The instance name. |

**Sample body, as it arrives**

```json
[
  {
    "event": "Disconnected",
    "data": {
      "…": "whatsmeow's events.Disconnected."
    },
    "instanceToken": "<instance token>",
    "instanceId": "<user id>",
    "instanceName": "apibrasil-whatsmeow"
  }
]
```

### `LoggedOut` — The session was unlinked from the handset

**When it fires:** When someone unlinks the device from WhatsApp. Unlike Disconnected: reconnecting does not fix this, you have to pair again. Enabled by the CONNECTION category in subscribe.

**Fields**

| Name | Type | Meaning |
| --- | --- | --- |
| `event` | string | "LoggedOut" — in PascalCase, NOT the uppercase used in subscribe. |
| `data` | object | whatsmeow's events.LoggedOut, with the reason for the disconnection. |
| `instanceToken` | string | The instance token, INSIDE the body. This is what you check the origin against — there is no signature in this channel. |
| `instanceId` | string | The instance user's id. |
| `instanceName` | string | The instance name. |

**Sample body, as it arrives**

```json
[
  {
    "event": "LoggedOut",
    "data": {
      "…": "whatsmeow's events.LoggedOut, with the reason for the disconnection."
    },
    "instanceToken": "<instance token>",
    "instanceId": "<user id>",
    "instanceName": "apibrasil-whatsmeow"
  }
]
```

### `TemporaryBan` — The number was temporarily banned

**When it fires:** When WhatsApp applies a temporary block to the number. It is the channel's most expensive notification to ignore — for as long as it lasts, nothing is sent. Enabled by the CONNECTION category in subscribe.

**Fields**

| Name | Type | Meaning |
| --- | --- | --- |
| `event` | string | "TemporaryBan" — in PascalCase, NOT the uppercase used in subscribe. |
| `data` | object | whatsmeow's events.TemporaryBan, with the code and duration of the block. |
| `instanceToken` | string | The instance token, INSIDE the body. This is what you check the origin against — there is no signature in this channel. |
| `instanceId` | string | The instance user's id. |
| `instanceName` | string | The instance name. |

**Sample body, as it arrives**

```json
[
  {
    "event": "TemporaryBan",
    "data": {
      "…": "whatsmeow's events.TemporaryBan, with the code and duration of the block."
    },
    "instanceToken": "<instance token>",
    "instanceId": "<user id>",
    "instanceName": "apibrasil-whatsmeow"
  }
]
```

### `PairSuccess` — Pairing completed

**When it fires:** When the QR code scan finishes and the device is linked. Enabled by the CONNECTION category in subscribe.

**Fields**

| Name | Type | Meaning |
| --- | --- | --- |
| `event` | string | "PairSuccess" — in PascalCase, NOT the uppercase used in subscribe. |
| `data` | object | whatsmeow's events.PairSuccess, with the JID that just paired. |
| `instanceToken` | string | The instance token, INSIDE the body. This is what you check the origin against — there is no signature in this channel. |
| `instanceId` | string | The instance user's id. |
| `instanceName` | string | The instance name. |

**Sample body, as it arrives**

```json
[
  {
    "event": "PairSuccess",
    "data": {
      "…": "whatsmeow's events.PairSuccess, with the JID that just paired."
    },
    "instanceToken": "<instance token>",
    "instanceId": "<user id>",
    "instanceName": "apibrasil-whatsmeow"
  }
]
```

### `CallOffer` — Incoming call

**When it fires:** When someone calls. The CALL category also covers CallAccept, CallTerminate, CallOfferNotice and CallRelayLatency, each with its own name in event. Enabled by the CALL category in subscribe.

**Fields**

| Name | Type | Meaning |
| --- | --- | --- |
| `event` | string | "CallOffer" — in PascalCase, NOT the uppercase used in subscribe. |
| `data` | object | whatsmeow's events.CallOffer: who called, when, and the call's id. |
| `instanceToken` | string | The instance token, INSIDE the body. This is what you check the origin against — there is no signature in this channel. |
| `instanceId` | string | The instance user's id. |
| `instanceName` | string | The instance name. |

**Sample body, as it arrives**

```json
[
  {
    "event": "CallOffer",
    "data": {
      "…": "whatsmeow's events.CallOffer: who called, when, and the call's id."
    },
    "instanceToken": "<instance token>",
    "instanceId": "<user id>",
    "instanceName": "apibrasil-whatsmeow"
  }
]
```

### `GroupInfo` — A group changed

**When it fires:** When a group's name, description, participants or permissions change. JoinedGroup, in the same category, tells you that you joined one. Enabled by the GROUP category in subscribe.

**Fields**

| Name | Type | Meaning |
| --- | --- | --- |
| `event` | string | "GroupInfo" — in PascalCase, NOT the uppercase used in subscribe. |
| `data` | object | whatsmeow's events.GroupInfo, with what changed in the group. |
| `instanceToken` | string | The instance token, INSIDE the body. This is what you check the origin against — there is no signature in this channel. |
| `instanceId` | string | The instance user's id. |
| `instanceName` | string | The instance name. |

**Sample body, as it arrives**

```json
[
  {
    "event": "GroupInfo",
    "data": {
      "…": "whatsmeow's events.GroupInfo, with what changed in the group."
    },
    "instanceToken": "<instance token>",
    "instanceId": "<user id>",
    "instanceName": "apibrasil-whatsmeow"
  }
]
```

### `Contact` — A contact changed

**When it fires:** When a contact's details are updated. PushName, in the same category, reports a display-name change. Enabled by the CONTACT category in subscribe.

**Fields**

| Name | Type | Meaning |
| --- | --- | --- |
| `event` | string | "Contact" — in PascalCase, NOT the uppercase used in subscribe. |
| `data` | object | whatsmeow's events.Contact. |
| `instanceToken` | string | The instance token, INSIDE the body. This is what you check the origin against — there is no signature in this channel. |
| `instanceId` | string | The instance user's id. |
| `instanceName` | string | The instance name. |

**Sample body, as it arrives**

```json
[
  {
    "event": "Contact",
    "data": {
      "…": "whatsmeow's events.Contact."
    },
    "instanceToken": "<instance token>",
    "instanceId": "<user id>",
    "instanceName": "apibrasil-whatsmeow"
  }
]
```

### `ChatPresence` — Typing or recording

**When it fires:** When the contact starts or stops typing in a conversation. Archive, in the same category, reports that the conversation was archived. Enabled by the CHAT_PRESENCE category in subscribe.

**Fields**

| Name | Type | Meaning |
| --- | --- | --- |
| `event` | string | "ChatPresence" — in PascalCase, NOT the uppercase used in subscribe. |
| `data` | object | whatsmeow's events.ChatPresence: the conversation, the state and for how long. |
| `instanceToken` | string | The instance token, INSIDE the body. This is what you check the origin against — there is no signature in this channel. |
| `instanceId` | string | The instance user's id. |
| `instanceName` | string | The instance name. |

**Sample body, as it arrives**

```json
[
  {
    "event": "ChatPresence",
    "data": {
      "…": "whatsmeow's events.ChatPresence: the conversation, the state and for how long."
    },
    "instanceToken": "<instance token>",
    "instanceId": "<user id>",
    "instanceName": "apibrasil-whatsmeow"
  }
]
```

### `Presence` — Online or last seen

**When it fires:** When a contact comes online or goes away. It is the channel's most frequent event. Enabled by the PRESENCE category in subscribe.

**Fields**

| Name | Type | Meaning |
| --- | --- | --- |
| `event` | string | "Presence" — in PascalCase, NOT the uppercase used in subscribe. |
| `data` | object | whatsmeow's events.Presence. |
| `instanceToken` | string | The instance token, INSIDE the body. This is what you check the origin against — there is no signature in this channel. |
| `instanceId` | string | The instance user's id. |
| `instanceName` | string | The instance name. |

**Sample body, as it arrives**

```json
[
  {
    "event": "Presence",
    "data": {
      "…": "whatsmeow's events.Presence."
    },
    "instanceToken": "<instance token>",
    "instanceId": "<user id>",
    "instanceName": "apibrasil-whatsmeow"
  }
]
```

### `HistorySync` — History sync

**When it fires:** When WhatsApp sends batches of history — usually right after pairing. The payloads are large; subscribe to this category only if you are going to use them. Enabled by the HISTORY_SYNC category in subscribe.

**Fields**

| Name | Type | Meaning |
| --- | --- | --- |
| `event` | string | "HistorySync" — in PascalCase, NOT the uppercase used in subscribe. |
| `data` | object | whatsmeow's events.HistorySync, with the batch of conversations and messages. |
| `instanceToken` | string | The instance token, INSIDE the body. This is what you check the origin against — there is no signature in this channel. |
| `instanceId` | string | The instance user's id. |
| `instanceName` | string | The instance name. |

**Sample body, as it arrives**

```json
[
  {
    "event": "HistorySync",
    "data": {
      "…": "whatsmeow's events.HistorySync, with the batch of conversations and messages."
    },
    "instanceToken": "<instance token>",
    "instanceId": "<user id>",
    "instanceName": "apibrasil-whatsmeow"
  }
]
```

### `Picture` — Profile picture changed

**When it fires:** When a contact or group changes its picture. Enabled by the PICTURE category in subscribe.

**Fields**

| Name | Type | Meaning |
| --- | --- | --- |
| `event` | string | "Picture" — in PascalCase, NOT the uppercase used in subscribe. |
| `data` | object | whatsmeow's events.Picture, with the JID and the new picture's id. |
| `instanceToken` | string | The instance token, INSIDE the body. This is what you check the origin against — there is no signature in this channel. |
| `instanceId` | string | The instance user's id. |
| `instanceName` | string | The instance name. |

**Sample body, as it arrives**

```json
[
  {
    "event": "Picture",
    "data": {
      "…": "whatsmeow's events.Picture, with the JID and the new picture's id."
    },
    "instanceToken": "<instance token>",
    "instanceId": "<user id>",
    "instanceName": "apibrasil-whatsmeow"
  }
]
```

### `LabelEdit` — Label created or changed

**When it fires:** When a WhatsApp Business label changes. The LABEL category also covers LabelAssociationChat and LabelAssociationMessage, which attach the label to a conversation or a message. Enabled by the LABEL category in subscribe.

**Fields**

| Name | Type | Meaning |
| --- | --- | --- |
| `event` | string | "LabelEdit" — in PascalCase, NOT the uppercase used in subscribe. |
| `data` | object | whatsmeow's events.LabelEdit. |
| `instanceToken` | string | The instance token, INSIDE the body. This is what you check the origin against — there is no signature in this channel. |
| `instanceId` | string | The instance user's id. |
| `instanceName` | string | The instance name. |

**Sample body, as it arrives**

```json
[
  {
    "event": "LabelEdit",
    "data": {
      "…": "whatsmeow's events.LabelEdit."
    },
    "instanceToken": "<instance token>",
    "instanceId": "<user id>",
    "instanceName": "apibrasil-whatsmeow"
  }
]
```

### `NewsletterJoin` — Joined a channel

**When it fires:** When the instance joins a WhatsApp channel. NewsletterLeave, in the same category, reports leaving one. Enabled by the NEWSLETTER category in subscribe.

**Fields**

| Name | Type | Meaning |
| --- | --- | --- |
| `event` | string | "NewsletterJoin" — in PascalCase, NOT the uppercase used in subscribe. |
| `data` | object | whatsmeow's events.NewsletterJoin. |
| `instanceToken` | string | The instance token, INSIDE the body. This is what you check the origin against — there is no signature in this channel. |
| `instanceId` | string | The instance user's id. |
| `instanceName` | string | The instance name. |

**Sample body, as it arrives**

```json
[
  {
    "event": "NewsletterJoin",
    "data": {
      "…": "whatsmeow's events.NewsletterJoin."
    },
    "instanceToken": "<instance token>",
    "instanceId": "<user id>",
    "instanceName": "apibrasil-whatsmeow"
  }
]
```

### `UserAbout` — About text changed

**When it fires:** When a contact changes their profile's about text. Enabled by the USER_ABOUT category in subscribe.

**Fields**

| Name | Type | Meaning |
| --- | --- | --- |
| `event` | string | "UserAbout" — in PascalCase, NOT the uppercase used in subscribe. |
| `data` | object | whatsmeow's events.UserAbout. |
| `instanceToken` | string | The instance token, INSIDE the body. This is what you check the origin against — there is no signature in this channel. |
| `instanceId` | string | The instance user's id. |
| `instanceName` | string | The instance name. |

**Sample body, as it arrives**

```json
[
  {
    "event": "UserAbout",
    "data": {
      "…": "whatsmeow's events.UserAbout."
    },
    "instanceToken": "<instance token>",
    "instanceId": "<user id>",
    "instanceName": "apibrasil-whatsmeow"
  }
]
```

## What usually bites

- **There is ONE URL, and it is picked by fill order** — The gateway takes the first non-empty of webhook_wh_qr_code, webhook_wh_connect, webhook_wh_message and webhook_wh_status — in that order. Filling all four, as you would on Wpp, sends everything to the QR code one. Fill in only the one you intend to use.
- **event comes in PascalCase; subscribe uses UPPERCASE** — Two vocabularies in the same channel. You subscribe to "MESSAGE", "READ_RECEIPT", "CONNECTION" — and receive event holding "Message", "Receipt", "Connected". Comparing event === "MESSAGE" never matches, and the failure is silent: the webhook arrives and your router ignores it.
- **The official documentation and the code disagree here** — The evolution-go wiki shows an example with "event": "MESSAGE" and a three-field envelope. The code builds "event": "Message" and five fields, with instanceToken, instanceId and instanceName. What is documented here is the code's version, which is what actually arrives.
- **The URL is registered on CONNECT, not in the dashboard or on the send** — The webhookUrl goes in the payload of the instance's connect action. Changing the URL afterwards requires connecting again — editing the field on the device is not enough.
- **No signature: what identifies the caller is instanceToken, in the body** — There is no HMAC as on Wpp and no custom header as on Baileys. The only header is Content-Type. You verify authenticity by comparing the body's instanceToken with yours, and that comparison must be constant-time. Since it is a secret travelling in the payload, HTTPS here is not a recommendation.
- **Five attempts, and no timeout on their side** — The producer retries up to 5 times, 30 seconds apart, and accepts any 2xx as success — not just 200. On the other hand its HTTP client has no timeout: an endpoint that never responds can hold the connection open indefinitely.

## Good practices

- **Compare instanceToken before trusting anything** — It is this channel's only proof of origin — there is no signature and no custom header. Compare it in constant time and reject anything that does not match, otherwise your URL accepts any POST from whoever finds it.
- **Route on event in PascalCase** — You subscribe to MESSAGE and receive "Message". A switch written with subscribe's names matches nothing, and the effect is an endpoint that receives everything and handles nothing.
- **Filter in subscribe, not in your code** — Presence and HistorySync are WhatsApp's highest-volume events. Receiving them just to drop them in an if burns bandwidth, CPU and the producer's five attempts. Subscribe only to the categories you use.
- **Respond 2xx, and respond fast** — Any 2xx counts as success. Without one it is 5 attempts 30 seconds apart for the SAME event — a slow endpoint turns into five copies over two and a half minutes.

## Sample receivers

**Node**

```node
app.post("/webhook/whatsmeow", express.json(), (req, res) => {
  const { event, data, instanceToken, instanceName } = req.body

  // This channel's only proof of origin: there is no signature.
  if (instanceToken !== INSTANCE_TOKEN) return res.sendStatus(401)

  res.sendStatus(200) // any 2xx works; without one, 5 attempts every 30s

  // CAREFUL: event comes in PascalCase, not subscribe's uppercase.
  switch (event) {
    case "Message":
      console.log("message received", data)
      break
    case "Receipt":
      console.log("read receipt", data)
      break
    case "QRCode":
      showQrCode(data.qrcode) // `${data.count}/${data.maxCount}` attempts
      break
    case "LoggedOut":
      // Unlike Disconnected: here you have to pair again.
      requestNewPairing(instanceName)
      break
    case "TemporaryBan":
      alertBan(instanceName, data)
      break
    default:
      console.warn("unhandled WhatsMeow event:", event)
  }
})
```

**Python**

```python
import hmac

@app.post("/webhook/whatsmeow")
def webhook_whatsmeow():
    body = request.get_json(force=True)

    # This channel's only proof of origin: there is no signature.
    if not hmac.compare_digest(body.get("instanceToken", ""), INSTANCE_TOKEN):
        return "", 401

    event = body["event"]   # PascalCase, not subscribe's uppercase
    data = body["data"]

    if event == "Message":
        print("message received", data)
    elif event == "Receipt":
        print("read receipt", data)
    elif event == "QRCode":
        show_qrcode(data["qrcode"])  # data['count'] of data['maxCount']
    elif event == "LoggedOut":
        # Unlike Disconnected: here you have to pair again.
        request_new_pairing(body["instanceName"])
    elif event == "TemporaryBan":
        alert_ban(body["instanceName"], data)
    else:
        print("unhandled WhatsMeow event:", event)

    return "", 200
```

**PHP**

```php
<?php

$body = json_decode(file_get_contents('php://input'), true);

// This channel's only proof of origin: there is no signature.
if (!hash_equals(INSTANCE_TOKEN, $body['instanceToken'] ?? '')) {
    http_response_code(401);
    exit;
}

http_response_code(200);

// CAREFUL: event comes in PascalCase, not subscribe's uppercase.
switch ($body['event']) {
    case 'Message':
        error_log('message received');
        break;
    case 'Receipt':
        error_log('read receipt');
        break;
    case 'QRCode':
        showQrCode($body['data']['qrcode']);
        break;
    case 'LoggedOut':
        // Unlike Disconnected: here you have to pair again.
        requestNewPairing($body['instanceName']);
        break;
    case 'TemporaryBan':
        alertBan($body['instanceName'], $body['data']);
        break;
    default:
        error_log('unhandled WhatsMeow event: ' . $body['event']);
}
```

## 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-whatsmeow
