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 lifecycle —
number.created,number.connected,number.disconnected,number.removedare subscribable on every number, whatever its provider. (number.disconnectedonly ever fires on native web numbers — seenumber.disconnected.) - Normalized call events —
call.ringing,call.connected,call.ended,call.missedon any calling-capable number. eventsis never optional. A webhook stored with an emptyeventsarray delivers nothing, on every vocabulary, and says nothing about it — it is a silent201. 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: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 messagemessage.received— message received on the connected number
message.group— message received in a groupmessage.newsletter— message received in a channel (@newsletter)message.stories— WhatsApp Status (story) posted by a contact
number.created— number created in Pilot Statusnumber.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 removednumber.recovered— number recovered from a degraded/blocked state (delivered only to"*"wildcard subscriptions)
"*" 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/connectedcall.ended— call finished (status:COMPLETED|FAILED|REJECTED;durationseconds when answered)call.missed— inbound call ended without being answeredcall.permission_updated— (Meta Cloud API only) reply to a call-permission request (status:NO_PERMISSION|TEMPORARY|PERMANENT)calls— (Meta Cloud API only) Meta’s nativecallsenvelope (rawentry[].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 benullfor failures that happen before the provider returns an ID.id— Pilot Status internal message ID. Same value as theidin theHTTP 202ofPOST /v1/messages/send.numberId— the public ID of the number that handled the event. On everynumber.*event it is now always the WhatsApp number ID (see the breaking change on thenumber.*family).correlationId— present when the event correlates to a prior send (same value as the202correlationId).quotedMessageId— onmessage.reply, themessageIdof the quoted original message (equals themessageIdof the originalmessage.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; plusgroupId/groupNameornewsletterId/newsletterName. - Status (
stories):from= the contact who posted the story (E.164). There is noto: 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
Includeserror and, when available, a stable errorCode (e.g. DELIVER_NOT_CONFIRMED).
message.received
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 incontent; 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.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):
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 inentry[].changes[].field.
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 inevents 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 events —
call.ringing,call.connected,call.ended,call.missed, andcall.permission_updated(Meta only). These are flat Pilot Status events, not Meta’s envelope — seecall.*payload. - Number lifecycle —
number.created,number.connected,number.removed.number.disconnectedis 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.
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:newsletterNameis the channel display name when available; otherwise it may be omitted. The channel identifier isnewsletterId(full...@newsletterJID).participantNameis the message author in the channel.- Media: on inbound media events,
mediaLinkis 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.mediaTypeis the category (image/video/audio/document/sticker),mediaCaptionthe accompanying text,mediaFilenamethe 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 toPOST /v1/messages/send; use them for provisioning/monitoring.data.numberIdis always the WhatsApp number ID on all of them — see the breaking change.number.disconnectedis 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
contentmay be empty; IDs and timestamps still exist. - The
message.readevent (and Read status in the API/Logs) only occurs when the recipient has WhatsApp read receipts enabled.