Webhook events
The four event types WAgate sends, with the payload of each.
Every delivery has the same envelope. The payload depends on type. See Webhooks for headers, signing and retries.
{
"id": "evt_<EVENT_ID>",
"type": "message",
"session": "<CHANNEL_ID>",
"timestamp": "2026-09-17T03:00:00Z",
"payload": {}
}| `type` | When it fires |
|---|---|
message | a message arrives, or your own number sends from the paired phone |
message.status | WhatsApp reports what happened to a message you sent |
group.participants | someone joins, leaves, is promoted or is demoted in a group |
session.status | the channel's connection state changes |
A receiver gets only the types it lists in events. A receiver with no events gets the default set: message, group.participants and session.status, not message.status. To get message.status, list it.
Ignore fields you do not know. Handle a type you do not recognize the way you handle an unknown message type: log it and answer 200. The test event webhook.test is the one extra type you will meet today.
Changes within /v1 are additive only: we add routes, optional fields, event types and error codes, and we never change or remove what is already there. We give 90 days' notice by email before any removal or rename.
`message`
A text received in a direct chat:
{
"id": "evt_<EVENT_ID>",
"type": "message",
"session": "<CHANNEL_ID>",
"timestamp": "2026-09-17T03:00:00Z",
"payload": {
"id": "3EB0C767D26A1D8E2A41",
"chat_jid": "[email protected]",
"sender_jid": "[email protected]",
"push_name": "Customer A",
"type": "text",
"text": "hello",
"from_me": false,
"is_group": false,
"timestamp": "2026-09-17T03:00:00Z"
}
}Fields :
| Field | Meaning |
|---|---|
id | the message ID |
chat_jid | the conversation: a person's JID or a group JID |
sender_jid | who sent it |
push_name | the sender's WhatsApp display name, when known |
type | the message kind, see below |
text | the text, or the caption of media, or the poll question |
media_id | present for messages with downloadable media; see Media |
from_me | true when your number sent it, for example from the paired phone |
is_group | true for a group chat |
timestamp | when WhatsApp says it was sent |
mentions | JIDs the message tags |
quoted_id, quoted_author | the message this one replies to, and its author |
sender_lid, sender_pn_resolved, chat_lid, chat_pn_resolved | see "People and LIDs" |
poll, reaction, revoke | extra objects for poll, reaction and delete messages |
Optional fields are absent, not empty. mentions absent means the message carried no context to read; [] means it tagged nobody.
Message `type` values
text, image, video, document, audio, sticker, contact, location, poll, poll_vote, reaction, protocol, short (a video note), group_invite, live_location, contact_list, unknown.
A message that cannot be described arrives as unknown. New types can be added, so treat a type you do not know the way you treat unknown.
Polls and votes
A poll arrives as type poll: text is the question and poll holds options. poll.multiple is present as true for a multiple-choice poll and omitted for a single-choice poll; absence means false.
{ "type": "poll", "text": "Which day suits you?", "poll": { "options": ["Monday", "Tuesday"] } }A vote arrives as type poll_vote: poll.poll_id and the chosen options by name in poll.selected.
{ "type": "poll_vote", "poll": { "poll_id": "3EB0C767D26A1D8E2A41", "selected": ["Tuesday"] } }poll.unresolvedcounts chosen options that could not be named, because the poll is not in this channel's log. A vote withunresolvedabove 0 is a vote you could not fully read, not a vote for fewer options.poll.unreadabletrue means the vote could not be decrypted, andselectedmeans nothing.- A readable vote with no
selectedand nounresolvedmeans the person withdrew their vote.
Reactions
A reaction arrives as type reaction. text is the emoji and reaction.message_id is the message reacted to. A removed reaction has an empty text and reaction.removed set to true.
{ "type": "reaction", "text": "馃憤", "reaction": { "message_id": "3EB0C767D26A1D8E2A41" } }Deleted messages
A message deleted for everyone arrives as type protocol with revoke.message_id, the message deleted. from_me true means your number deleted it on the phone. from_me false means its sender did, or in a group possibly an admin. A delete you make through the API is not echoed here.
{ "type": "protocol", "from_me": false, "revoke": { "message_id": "3EB0C767D26A1D8E2A41" } }People and LIDs
People are identified by phone number, as <number>@s.whatsapp.net with no device suffix, so the same person always has the same address.
WhatsApp sometimes addresses a person by a LID (<digits>@lid or <digits>@hosted.lid) instead. When it does:
sender_jid, thechat_jidof a chat that is not a group, and each entry ofparticipantsstill carry the phone JID whenever WAgate knows it.- The LID the address arrived as is in
sender_lid,chat_lidorparticipant_lids. - When no phone number is known for a LID, the field keeps the LID and a flag says so:
sender_pn_resolvedorchat_pn_resolvedis present andfalse. These flags appear only in that case. Treat such a value as unresolved, not as a phone number. - A group
chat_jid,mentionsandquoted_authorare delivered as WhatsApp sent them.
`message.status`
What happened to a message you sent. A server_ack from the send call only means WhatsApp's servers took it; this event is the other side.
{
"id": "evt_<EVENT_ID>",
"type": "message.status",
"session": "<CHANNEL_ID>",
"timestamp": "2026-09-17T03:00:05Z",
"payload": {
"ids": ["3EB0C767D26A1D8E2A41"],
"chat_jid": "[email protected]",
"recipient_jid": "[email protected]",
"status": "delivered",
"timestamp": "2026-09-17T03:00:04Z"
}
}ids: the messages this receipt covers. WhatsApp batches, so one receipt often covers several sends.status:delivered,read,playedorundecryptable.- Receipts from your own other devices are not sent.
`group.participants`
{
"id": "evt_<EVENT_ID>",
"type": "group.participants",
"session": "<CHANNEL_ID>",
"timestamp": "2026-09-17T03:00:00Z",
"payload": {
"group_jid": "[email protected]",
"action": "join",
"participants": ["[email protected]"],
"actor_jid": "[email protected]",
"timestamp": "2026-09-17T03:00:00Z"
}
}actionisjoin,leave,promoteordemote. These differ from theaddandremoveyou send to the participants route.participant_lidsandparticipants_pn_resolvedline up withparticipantsby index. A participant that arrived as a phone number has""inparticipant_lids.participant_lidsis present only when at least one participant arrived as a LID, andparticipants_pn_resolvedonly when at least one could not be resolved.actor_jidis who made the change, when known.
`session.status`
The channel's connection state changed.
{
"id": "evt_<EVENT_ID>",
"type": "session.status",
"session": "<CHANNEL_ID>",
"timestamp": "2026-09-17T03:00:00Z",
"payload": {
"state": "DISCONNECTED",
"jid": "[email protected]",
"reason": "..."
}
}state is one of NEW, PAIRING, CONNECTED, DISCONNECTED, BLOCKED. jid and reason appear when known.
Use it to alert yourself when a channel stops being CONNECTED.