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.

json
{
  "id": "evt_<EVENT_ID>",
  "type": "message",
  "session": "<CHANNEL_ID>",
  "timestamp": "2026-09-17T03:00:00Z",
  "payload": {}
}
`type`When it fires
messagea message arrives, or your own number sends from the paired phone
message.statusWhatsApp reports what happened to a message you sent
group.participantssomeone joins, leaves, is promoted or is demoted in a group
session.statusthe 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:

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

FieldMeaning
idthe message ID
chat_jidthe conversation: a person's JID or a group JID
sender_jidwho sent it
push_namethe sender's WhatsApp display name, when known
typethe message kind, see below
textthe text, or the caption of media, or the poll question
media_idpresent for messages with downloadable media; see Media
from_metrue when your number sent it, for example from the paired phone
is_grouptrue for a group chat
timestampwhen WhatsApp says it was sent
mentionsJIDs the message tags
quoted_id, quoted_authorthe message this one replies to, and its author
sender_lid, sender_pn_resolved, chat_lid, chat_pn_resolvedsee "People and LIDs"
poll, reaction, revokeextra 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.

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

json
{ "type": "poll_vote", "poll": { "poll_id": "3EB0C767D26A1D8E2A41", "selected": ["Tuesday"] } }
  • poll.unresolved counts chosen options that could not be named, because the poll is not in this channel's log. A vote with unresolved above 0 is a vote you could not fully read, not a vote for fewer options.
  • poll.unreadable true means the vote could not be decrypted, and selected means nothing.
  • A readable vote with no selected and no unresolved means 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.

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

json
{ "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, the chat_jid of a chat that is not a group, and each entry of participants still carry the phone JID whenever WAgate knows it.
  • The LID the address arrived as is in sender_lid, chat_lid or participant_lids.
  • When no phone number is known for a LID, the field keeps the LID and a flag says so: sender_pn_resolved or chat_pn_resolved is present and false. These flags appear only in that case. Treat such a value as unresolved, not as a phone number.
  • A group chat_jid, mentions and quoted_author are 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.

json
{
  "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, played or undecryptable.
  • Receipts from your own other devices are not sent.

`group.participants`

json
{
  "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"
  }
}
  • action is join, leave, promote or demote. These differ from the add and remove you send to the participants route.
  • participant_lids and participants_pn_resolved line up with participants by index. A participant that arrived as a phone number has "" in participant_lids. participant_lids is present only when at least one participant arrived as a LID, and participants_pn_resolved only when at least one could not be resolved.
  • actor_jid is who made the change, when known.

`session.status`

The channel's connection state changed.

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

Webhook events 路 WAgate docs