Skip to main content

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.

Which events your number sends

The event vocabulary depends on how the number was connected. A number paired by QR code speaks the canonical Pilot Status vocabulary documented on this page; a Meta Cloud API number forwards Meta’s own native envelope instead. They are different formats with different names, and no subscription setting converts one into the other. Three things are the same on every vocabulary:
  • Number lifecyclenumber.created, number.connected, number.disconnected, number.removed are subscribable on every number, whatever its provider. (number.disconnected only ever fires on native web numbers — see number.disconnected.)
  • Normalized call eventscall.ringing, call.connected, call.ended, call.missed on any calling-capable number.
  • events is never optional. A webhook stored with an empty events array delivers nothing, on every vocabulary, and says nothing about it — it is a silent 201. Subscribe to the names you want, or to the "*" wildcard.
Not sure which kind your number is? GET /v1/numbers returns provider per number: PILOT_STATUS for native web, META for Cloud API. See Official vs unofficial.

Payload format

Every event arrives as JSON:
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 (when the message happened, per the provider — not our ingestion time).
Old messages are not replayed. When a number connects, WhatsApp hands the provider the device’s history. Those messages are imported into the chat, but they are not delivered to your webhook — otherwise every reconnect would re-deliver up to 30 days of conversation and nothing in the payload would let you tell it apart from traffic that just arrived.Opt in per number with settings.webhookHistoricalMessages on PATCH /v1/numbers/{id}. To read history instead, use GET /v1/messages/history.

Available events

Outbound (delivery/status): message.sent, message.delivered, message.read, message.failed Inbound (received messages):
  • message.reply — reply that quotes another message
  • message.received — message received on the connected number
message.reply is exclusive — it does NOT also fire message.received.When a contact replies quoting an earlier message (swipe-to-reply in WhatsApp), the event is message.reply and only that one. If your webhook subscribes to message.received but not to message.reply, those messages never reach you — and nothing reports an error, so it looks like the customer never wrote.Subscribe to both, or use the * wildcard, if you want every inbound message.
  • message.group — message received in a group
  • message.newsletter — message received in a channel (@newsletter)
  • message.stories — WhatsApp Status (story) posted by a contact
Number lifecycle:
  • number.created — number created in Pilot Status
  • number.connected — connected to WhatsApp (OPEN state)
  • number.disconnected — connection drop confirmed by the number-health transition, on native web (unofficial) numbers (one event per confirmed drop, not one per connectivity blip).
  • number.removed — number removed
  • number.recovered — number recovered from a degraded/blocked state (delivered only to "*" wildcard subscriptions)
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): The normalized call events — call.ringing, call.connected, call.ended, call.missed — are available on any calling-capable number (native web (unofficial) — 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 that handled the event. On every number.* event it is now always the WhatsApp number ID (see the breaking change on the number.* family).
  • 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: 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

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.
  • Status (stories): from = the contact who posted the story (E.164). There is no to: a Status is broadcast to the poster’s contact list, not addressed to your number.

message.sent

message.delivered

message.read

message.read only fires when the recipient has WhatsApp read receipts enabled.

message.failed

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

message.received

Media example. On inbound events (message.received / message.reply) that carry media, mediaLink is a durable, publicly-fetchable URL to the media re-hosted on Pilot Status storage — for all providers (Evolution GO, Evolution v2, and Meta Cloud API), including WhatsApp end-to-end-encrypted media, which is decrypted server-side before re-hosting. The media* fields are omitted on text-only messages.

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.

message.group

message.newsletter

message.stories

WhatsApp Status (stories) posted by a contact the connected phone can see.
Only delivered while someone is subscribed. Status traffic is switched off at the provider by default (ignoreStatus); subscribing a webhook to message.stories — or to the "*" wildcard — is what turns it on for that number. Remove the last subscription and the provider stops sending Status again.
A Status is not persisted. It does not create a conversation or a message in your chat history, and it does not appear in GET /v1/messages. WhatsApp expires stories after 24h; the mediaLink we re-host is durable, the story is not.

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):
Breaking change — data.numberId is always the number ID. On every number.* event, data.numberId is now the WhatsApp number ID. Previously number.created, number.connected and number.removed carried the internal instance ID, while the health events already carried the number ID: two opaque IDs in the same field, so cross-referencing the two families silently never matched. If your consumer matched lifecycle events by instance ID, it will stop matching — key it on the number ID instead. It is the same id GET /v1/numbers returns.

number.created

number.connected

number.disconnected

Emitted from the number-health transition, which owns the anti-flap window and the one-alert-per-transition dedup: you get one event per confirmed drop, not one per socket blip.
  • Native web (unofficial) numbers: this event fires on a confirmed connection drop. Previously an unofficial number that fell off produced no subscribable event at all — only the "*"-only health events.
  • Meta Cloud API numbers: they never emit number.disconnected. A Meta quality/status transition emits only its number-health event (number.health_blocked / number.health_degraded / number.health_shadowban), which reaches "*" subscriptions.
error and errorCode are included when the health transition carries a reason.

number.removed

call.* payload

Unlike message.* / number.*, the normalized call.* events are flat (no data wrapper):
See the full calling flow.

Meta Cloud API events (native envelope)

A number connected through the Meta Cloud API — Embedded Signup or pasted credentials — does not send the canonical events above. It forwards Meta’s own webhook envelope, verbatim, keyed by the field name Meta puts in entry[].changes[].field.
message.received never fires on a Meta number, and events: ["*"] does not convert the format. The event name you subscribe to is Meta’s field name — messages — and the body you receive is Meta’s envelope. A receiver written against the canonical schema ({ event, data }) reads undefined out of a Meta payload and reports nothing, which looks exactly like “the customer never wrote”. Write the parser for the native shape, or branch on the number’s provider.
Native envelope (as delivered)
entry[].changes[].value is Meta’s payload for that field, unmodified — the same shape Meta’s own reference documents. Nothing is added to it and nothing is removed, so a receiver already written against Meta’s webhooks keeps working. One change per delivery. Meta may batch several changes into one entry; each one is subscription-matched and delivered on its own, so entry and entry[0].changes always have exactly one element. Keep the loops your Meta parser already has — they will simply always run once — and never assume two changes in one request are related.

Subscribable fields

These are the field names you can put in events on POST /v1/webhooks for a Meta number. They are also the names that arrive as the delivered event. Plus the two families shared with every other vocabulary:
  • Normalized call eventscall.ringing, call.connected, call.ended, call.missed, and call.permission_updated (Meta only). These are flat Pilot Status events, not Meta’s envelope — see call.* payload.
  • Number lifecyclenumber.created, number.connected, number.removed. number.disconnected is subscribable but never fires on a Meta number: a Meta quality/status transition emits a number-health event instead, and those reach "*" subscriptions only.
Nine of Meta’s fields are never delivered, not even to a "*" subscription: account_alerts, automatic_events, history, messaging_handovers, partner_solutions, security, smb_app_state_sync, standby and tracking_events. They are internal (bulk history backfill, coexistence state sync) or admin/security traffic, and they are not offered in the event picker either. Everything else Meta sends for a subscribed field is forwarded.

Migration-layer events (Evolution GO and v2)

A number you brought over through a compatibility layer keeps that provider’s own event vocabulary — the point of the layer is that your existing handlers do not change:
  • Evolution GO — native PascalCase (Message, SendMessage, Receipt, Presence, …), with the UPPER_SNAKE aliases also accepted. Full list on the Evolution GO layer page.
  • Evolution v2 — native dotted names (messages.upsert, messages.update, connection.update, …). Full list on the Evolution V2 layer page.
Both also carry the shared normalized call.* and number.* families described above.
Group and channel events on these two need a paid plan: groups.upsert, groups.update, group-participants.update (v2) and GroupInfo, JoinedGroup, NewsletterJoin, NewsletterLeave (GO) are hidden from the event picker on a free account. The canonical message.group and message.newsletter are not restricted.

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: on inbound media events, mediaLink is a durable, publicly-fetchable URL to the re-hosted media — for all providers (Evolution GO, Evolution v2, Meta Cloud API), including WhatsApp end-to-end-encrypted media decrypted server-side. mediaType is the category (image/video/audio/document/sticker), mediaCaption the accompanying text, mediaFilename the original filename (documents). All four are omitted on text-only messages and when a number’s PII redaction blanks content.
  • number.* events are not correlated to POST /v1/messages/send; use them for provisioning/monitoring. data.numberId is always the WhatsApp number ID on all of them — see the breaking change.
  • number.disconnected is emitted from the number-health transition (one event per confirmed drop, not one per connection flap), and only for native web (unofficial) numbers; Meta Cloud API numbers signal trouble through the number-health events instead.
  • 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.