> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pilotstatus.com.br/llms.txt
> Use this file to discover all available pages before exploring further.

# Pilot Status Webhook Events — Complete Reference

> Reference for all Pilot Status webhook events: message received, sent, delivered, read, failed, and number connection events with payload schemas.

# Webhook event reference

Webhooks deliver real-time events to your system's URL. This page documents every event, its payload, and how to correlate events with your API calls. To create and manage webhooks, see [Configure webhooks](/api/webhooks/configure).

## Payload format

Every event arrives as JSON:

```json theme={null}
{
  "event": "message.sent",
  "data": {}
}
```

**Schema v3 (camelCase).** All `data` fields are camelCase. Phone numbers are always **E.164 with `+`** (no device suffix, no `@s.whatsapp.net` / `@lid`). Timestamps are always **ISO 8601** in the single `createdAt` field (the time the event happened).

## Available events

**Outbound (delivery/status):** `message.sent`, `message.delivered`, `message.read`, `message.failed`

**Inbound (received messages):**

* `message.reply` — correlated reply
* `message.received` — message received on the connected number
* `message.group` — message received in a group
* `message.newsletter` — message received in a channel (`@newsletter`)

**Number lifecycle:**

* `number.created` — instance created in Pilot Status
* `number.connected` — connected to WhatsApp (OPEN state)
* `number.disconnected` — disconnect confirmed after periodic health check (does not reflect every brief connectivity fluctuation)
* `number.removed` — instance removed
* `number.recovered` — number recovered from a degraded/blocked state

**Health events (`"*"` subscription only):** the health transition events — `number.health_blocked`, `number.health_degraded`, `number.health_shadowban` — and `number.recovered` are **not individually subscribable** and do not appear in the event picker. They are delivered **only** to webhooks subscribed with the `"*"` wildcard.

**Voice calls** (WhatsApp Business Calling, see [Voice Calls](/api/calls/overview)):

The normalized call events — `call.ringing`, `call.connected`, `call.ended`, `call.missed` — are available on **any calling-capable number** (native web / Evolution GO **and** Meta Cloud API). `call.permission_updated` and the native `calls` envelope are **Meta Cloud API only**.

* `call.ringing` — call ringing (inbound UIC or outbound BIC transition)
* `call.connected` — call answered/connected
* `call.ended` — call finished (`status`: `COMPLETED` | `FAILED` | `REJECTED`; `duration` seconds when answered)
* `call.missed` — inbound call ended without being answered
* `call.permission_updated` — (**Meta Cloud API only**) reply to a call-permission request (`status`: `NO_PERMISSION` | `TEMPORARY` | `PERMANENT`)
* `calls` — (**Meta Cloud API only**) Meta's **native** `calls` envelope (raw `entry[].changes[].value`, carrying SDPs) for custom WebRTC signaling

## Identifiers and correlation

Every message event carries two distinct IDs:

* `messageId` — WhatsApp/provider message ID (e.g. `key.id`). May be `null` for failures that happen before the provider returns an ID.
* `id` — Pilot Status internal message ID. Same value as the `id` in the `HTTP 202` of `POST /v1/messages/send`.
* `numberId` — the **public** ID of the number (instance) that handled the event — the same `id` exposed by `GET /v1/numbers`.
* `correlationId` — present when the event correlates to a prior send (same value as the `202` `correlationId`).
* `quotedMessageId` — on `message.reply`, the `messageId` of the quoted original message (equals the `messageId` of the original `message.sent`).

### Correlation with `POST /v1/messages/send`

After an accepted send, the API returns **HTTP 202** with **`id`** and **`correlationId`**:

| `202` field         | Webhook equivalent                                                                                                    |
| ------------------- | --------------------------------------------------------------------------------------------------------------------- |
| **`id`**            | **`id`** on `message.sent` / `message.delivered` / `message.read` / `message.failed` (same value).                    |
| **`correlationId`** | May repeat on outbound status events and on **`message.reply`** / **`message.received`** when correlated to the send. |
| *(not in `202`)*    | WhatsApp **`messageId`** — only appears from `message.sent` onward.                                                   |

On **`message.reply`**: **`quotedMessageId`** = the **`messageId`** of the original **`message.sent`**; the reply's own **`messageId`** is the **new** inbound message. Use `quotedMessageId` (and `correlationId` when present) to match the reply to your prior send.

## `message.*` payload

### Common fields

| Field             | Presence    | Description                                                                                    |
| ----------------- | ----------- | ---------------------------------------------------------------------------------------------- |
| `event`           | always      | event name                                                                                     |
| `from`            | always      | sender in E.164                                                                                |
| `to`              | always      | recipient in E.164                                                                             |
| `numberId`        | always      | public ID of the number that handled the message                                               |
| `messageId`       | always      | WhatsApp/provider message ID (`null` before the provider returns one)                          |
| `id`              | always      | Pilot Status internal ID (= `id` from the `HTTP 202`)                                          |
| `type`            | always      | `text`, `image`, `audio`, `video`, `document`, `location`, `contacts`, `sticker` or `reaction` |
| `fromMe`          | always      | boolean — `true` on outbound events, `false` on inbound                                        |
| `createdAt`       | always      | ISO 8601 — event time                                                                          |
| `content`         | conditional | message text (or caption)                                                                      |
| `participantName` | conditional | sender's WhatsApp name (received / group / channel)                                            |
| `correlationId`   | conditional | when correlated to a prior send                                                                |
| `contentReplied`  | conditional | text of the quoted message (`message.reply` only)                                              |
| `quotedMessageId` | conditional | `messageId` of the quoted message (`message.reply` only)                                       |
| `mediaLink`       | conditional | media URL, when the provider exposes it                                                        |
| `mediaType`       | conditional | media type                                                                                     |
| `mediaCaption`    | conditional | media caption                                                                                  |
| `mediaFilename`   | conditional | media file name                                                                                |
| `groupName`       | conditional | group name (`message.group` only)                                                              |
| `groupId`         | conditional | group JID (`message.group` only)                                                               |
| `newsletterName`  | conditional | channel name (`message.newsletter` only)                                                       |
| `newsletterId`    | conditional | channel JID (`message.newsletter` only)                                                        |
| `error`           | conditional | error message (`message.failed` only)                                                          |
| `errorCode`       | conditional | stable error code (`message.failed` only)                                                      |

**`from`/`to` direction semantics:**

* **Outbound** (`sent`/`delivered`/`read`/`failed`): `to` = destination number (E.164); `from` = own number (present when cheaply resolvable, otherwise omitted); `fromMe` = `true`.
* **Inbound** (`received`/`reply`): `from` = contact/sender (E.164); `to` = own number (E.164); `fromMe` = `false`.
* **Group / channel** (`group`/`newsletter`): `from` = participant (E.164); `to` = own number; plus `groupId`/`groupName` or `newsletterId`/`newsletterName`.

### message.sent

```json theme={null}
{
  "event": "message.sent",
  "data": {
    "to": "+5511999999999",
    "from": "+5511888888888",
    "numberId": "cmm0abc123",
    "messageId": "A52298BB1619CB5EC464BEFB8A3ACB94",
    "id": "cmm04obm46zz0qv4ycjp8x6r2",
    "type": "text",
    "fromMe": true,
    "content": "sent text",
    "correlationId": "corr_123",
    "createdAt": "2026-02-24T15:00:05.000Z"
  }
}
```

### message.delivered

```json theme={null}
{
  "event": "message.delivered",
  "data": {
    "to": "+5511999999999",
    "from": "+5511888888888",
    "numberId": "cmm0abc123",
    "messageId": "A52298BB1619CB5EC464BEFB8A3ACB94",
    "id": "cmm04obm46zz0qv4ycjp8x6r2",
    "type": "text",
    "fromMe": true,
    "content": "sent text",
    "createdAt": "2026-02-24T15:00:06.000Z"
  }
}
```

### message.read

<Note>
  `message.read` only fires when the recipient has **WhatsApp read receipts** enabled.
</Note>

```json theme={null}
{
  "event": "message.read",
  "data": {
    "to": "+5511999999999",
    "from": "+5511888888888",
    "numberId": "cmm0abc123",
    "messageId": "A52298BB1619CB5EC464BEFB8A3ACB94",
    "id": "cmm04obm46zz0qv4ycjp8x6r2",
    "type": "text",
    "fromMe": true,
    "content": "sent text",
    "createdAt": "2026-02-24T15:00:10.000Z"
  }
}
```

### message.failed

Includes `error` and, when available, a stable `errorCode` (e.g. `DELIVER_NOT_CONFIRMED`).

```json theme={null}
{
  "event": "message.failed",
  "data": {
    "to": "+5511999999999",
    "from": "+5511888888888",
    "numberId": "cmm0abc123",
    "messageId": null,
    "id": "cmm04obm46zz0qv4ycjp8x6r2",
    "type": "text",
    "fromMe": true,
    "content": "sent text",
    "error": "Failed to send message via Pilot Status.",
    "errorCode": "DELIVER_NOT_CONFIRMED",
    "createdAt": "2026-02-24T15:00:05.000Z"
  }
}
```

### message.received

```json theme={null}
{
  "event": "message.received",
  "data": {
    "from": "+5511999999999",
    "to": "+5511888888888",
    "numberId": "cmm0abc123",
    "messageId": "msg_in_id",
    "id": "cmm04obm46zz0qv4ycjp8x6r2",
    "type": "text",
    "fromMe": false,
    "content": "Hi",
    "participantName": "WhatsApp name",
    "createdAt": "2026-02-24T10:30:00.000Z"
  }
}
```

Media example (when the provider exposes the URL — e.g. Meta Cloud API). For providers without a public media URL, `mediaLink`/`mediaType`/`mediaCaption`/`mediaFilename` may be omitted.

```json theme={null}
{
  "event": "message.received",
  "data": {
    "from": "+5511999999999",
    "to": "+5511888888888",
    "numberId": "cmm0abc123",
    "messageId": "msg_in_id",
    "id": "cmm04obm46zz0qv4ycjp8x6r2",
    "type": "image",
    "fromMe": false,
    "content": "Check this photo",
    "mediaLink": "https://...",
    "mediaType": "image",
    "mediaCaption": "Check this photo",
    "mediaFilename": "photo.jpg",
    "createdAt": "2026-02-24T10:30:00.000Z"
  }
}
```

### message.reply

The contact's **new** text is in `content`; the **quoted** original message text is in `contentReplied`. Use `quotedMessageId` to match the `messageId` from your original outbound `message.sent`.

```json theme={null}
{
  "event": "message.reply",
  "data": {
    "from": "+5511999999999",
    "to": "+5511888888888",
    "numberId": "cmm0abc123",
    "messageId": "msg_12345",
    "id": "cmm04obm46zz0qv4ycjp8x6r2",
    "type": "text",
    "fromMe": false,
    "content": "Yes, I confirm",
    "contentReplied": "Hi! Do you confirm your appointment?",
    "quotedMessageId": "msg_original_123",
    "correlationId": "corr_123",
    "createdAt": "2026-02-24T10:30:00.000Z"
  }
}
```

### message.group

```json theme={null}
{
  "event": "message.group",
  "data": {
    "from": "+5511999999999",
    "to": "+5511888888888",
    "numberId": "cmm0abc123",
    "messageId": "msg_in_id",
    "id": "cmm04obm46zz0qv4ycjp8x6r2",
    "type": "text",
    "fromMe": false,
    "content": "Message in the group",
    "participantName": "WhatsApp name",
    "groupId": "120363123456789012@g.us",
    "groupName": "My Group",
    "createdAt": "2026-02-24T10:30:00.000Z"
  }
}
```

### message.newsletter

```json theme={null}
{
  "event": "message.newsletter",
  "data": {
    "from": "+5511999999999",
    "to": "+5511888888888",
    "numberId": "cmm0abc123",
    "messageId": "msg_in_id",
    "id": "cmm04obm46zz0qv4ycjp8x6r2",
    "type": "text",
    "fromMe": false,
    "content": "Text in the channel",
    "participantName": "WhatsApp name",
    "newsletterId": "120363123456789012@newsletter",
    "newsletterName": "My Channel",
    "createdAt": "2026-02-24T10:30:00.000Z"
  }
}
```

## `number.*` payload

Fields for `number.created` / `number.connected` / `number.disconnected` / `number.removed` / `number.recovered` (and the `number.health_blocked` / `number.health_degraded` / `number.health_shadowban` events):

| Field         | Presence    | Description                                       |
| ------------- | ----------- | ------------------------------------------------- |
| `event`       | always      | event name                                        |
| `numberId`    | always      | public number ID (same `id` as `GET /v1/numbers`) |
| `phone`       | always      | number in E.164 with `+`                          |
| `displayName` | always      | display name of the number                        |
| `createdAt`   | always      | ISO 8601 — event time                             |
| `profileName` | conditional | WhatsApp profile name, when available             |
| `error`       | conditional | error message (health / failure events)           |
| `errorCode`   | conditional | stable error code, when present                   |

### number.created

```json theme={null}
{
  "event": "number.created",
  "data": {
    "numberId": "cmm0abc123",
    "phone": "+5511999999999",
    "displayName": "Main store",
    "createdAt": "2026-02-24T15:00:05.000Z"
  }
}
```

### number.connected

```json theme={null}
{
  "event": "number.connected",
  "data": {
    "numberId": "cmm0abc123",
    "phone": "+5511999999999",
    "displayName": "Main store",
    "profileName": "Main Store Official",
    "createdAt": "2026-02-24T15:00:05.000Z"
  }
}
```

### number.disconnected

```json theme={null}
{
  "event": "number.disconnected",
  "data": {
    "numberId": "cmm0abc123",
    "phone": "+5511999999999",
    "displayName": "Main store",
    "createdAt": "2026-02-24T15:10:00.000Z"
  }
}
```

### number.removed

```json theme={null}
{
  "event": "number.removed",
  "data": {
    "numberId": "cmm0abc123",
    "phone": "+5511999999999",
    "displayName": "Main store",
    "createdAt": "2026-02-24T15:10:00.000Z"
  }
}
```

## `call.*` payload

Unlike `message.*` / `number.*`, the normalized `call.*` events are **flat** (no `data` wrapper):

```json theme={null}
{
  "event": "call.ended",
  "callId": "call_01HZX...",
  "externalCallId": "wacid.ABGG...",
  "direction": "OUTBOUND",
  "status": "COMPLETED",
  "from": "5511888888888",
  "to": "5511999999999",
  "timestamp": "2026-07-03T15:02:05.000Z",
  "duration": 120
}
```

| Field            | Presence                                     | Description                                                                                                                                |
| ---------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `event`          | always                                       | `call.ringing` / `call.connected` / `call.ended` / `call.missed` / `call.permission_updated`                                               |
| `callId`         | always (`null` on `call.permission_updated`) | Pilot Status call id (use with `GET /v1/calls/{callId}`)                                                                                   |
| `externalCallId` | conditional                                  | Meta call id (`wacid...`) — correlates with the native `calls` envelope                                                                    |
| `direction`      | always (`null` on `call.permission_updated`) | `INBOUND` (user-initiated) or `OUTBOUND` (business-initiated)                                                                              |
| `status`         | always                                       | call status (`RINGING`/`ACCEPTED`/`COMPLETED`/`FAILED`/`MISSED`/`REJECTED`) or permission status (`NO_PERMISSION`/`TEMPORARY`/`PERMANENT`) |
| `from` / `to`    | always (nullable)                            | the two parties (digits)                                                                                                                   |
| `timestamp`      | always                                       | ISO 8601 — event time                                                                                                                      |
| `duration`       | conditional                                  | call duration in seconds (`call.ended`, only when answered)                                                                                |

See the [full calling flow](/api/calls/overview).

## Important notes

* **`message.newsletter`:** `newsletterName` is the channel display name when available; otherwise it may be omitted. The channel identifier is `newsletterId` (full `...@newsletter` JID). `participantName` is the message author in the channel.
* **Media:** `mediaLink`/`mediaType`/`mediaCaption`/`mediaFilename` appear only when the provider exposes that data (e.g. Meta Cloud API). For providers without a public media URL, they are omitted.
* `number.*` events are not correlated to `POST /v1/messages/send`; use them for provisioning/monitoring. `number.disconnected` is emitted after the health check confirms disconnect, not on every connection flap.
* Customer webhook payloads do not include internal fields such as `lastMessageId`.
* Delivery of sensitive fields can depend on retention configuration. With retention off, conditional fields such as `content` may be empty; IDs and timestamps still exist.
* The `message.read` event (and **Read** status in the API/Logs) only occurs when the recipient has **WhatsApp read receipts** enabled.
