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.

Payload format

Every event arrives as JSON:
{
  "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.health_* — number health transitions
Voice calls (WhatsApp Business Calling — Meta Cloud API numbers only, see Voice Calls):
  • 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 — reply to a call-permission request (status: NO_PERMISSION | TEMPORARY | PERMANENT)
  • calls — 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 fieldWebhook equivalent
idid on message.sent / message.delivered / message.read / message.failed (same value).
correlationIdMay 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

FieldPresenceDescription
eventalwaysevent name
fromalwayssender in E.164
toalwaysrecipient in E.164
numberIdalwayspublic ID of the number that handled the message
messageIdalwaysWhatsApp/provider message ID (null before the provider returns one)
idalwaysPilot Status internal ID (= id from the HTTP 202)
typealwaystext, image, audio, video, document, location, contacts, sticker or reaction
fromMealwaysboolean — true on outbound events, false on inbound
createdAtalwaysISO 8601 — event time
contentconditionalmessage text (or caption)
participantNameconditionalsender’s WhatsApp name (received / group / channel)
correlationIdconditionalwhen correlated to a prior send
contentRepliedconditionaltext of the quoted message (message.reply only)
quotedMessageIdconditionalmessageId of the quoted message (message.reply only)
mediaLinkconditionalmedia URL, when the provider exposes it
mediaTypeconditionalmedia type
mediaCaptionconditionalmedia caption
mediaFilenameconditionalmedia file name
groupNameconditionalgroup name (message.group only)
groupIdconditionalgroup JID (message.group only)
newsletterNameconditionalchannel name (message.newsletter only)
newsletterIdconditionalchannel JID (message.newsletter only)
errorconditionalerror message (message.failed only)
errorCodeconditionalstable 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

{
  "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

{
  "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

message.read only fires when the recipient has WhatsApp read receipts enabled.
{
  "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).
{
  "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

{
  "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.
{
  "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.
{
  "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

{
  "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

{
  "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 (and the number.health_* events):
FieldPresenceDescription
eventalwaysevent name
numberIdalwayspublic number ID (same id as GET /v1/numbers)
phonealwaysnumber in E.164 with +
displayNamealwaysdisplay name of the number
createdAtalwaysISO 8601 — event time
profileNameconditionalWhatsApp profile name, when available
errorconditionalerror message (health / failure events)
errorCodeconditionalstable error code, when present

number.created

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

number.connected

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

number.disconnected

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

number.removed

{
  "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):
{
  "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
}
FieldPresenceDescription
eventalwayscall.ringing / call.connected / call.ended / call.missed / call.permission_updated
callIdalways (null on call.permission_updated)Pilot Status call id (use with GET /v1/calls/{callId})
externalCallIdconditionalMeta call id (wacid...) — correlates with the native calls envelope
directionalways (null on call.permission_updated)INBOUND (user-initiated) or OUTBOUND (business-initiated)
statusalwayscall status (RINGING/ACCEPTED/COMPLETED/FAILED/MISSED/REJECTED) or permission status (NO_PERMISSION/TEMPORARY/PERMANENT)
from / toalways (nullable)the two parties (digits)
timestampalwaysISO 8601 — event time
durationconditionalcall duration in seconds (call.ended, only when answered)
See the full calling flow.

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.