WhatsApp Gateway
Guides

Event shape

The fields in every gateway event, plus a copy-pasteable JSON example for each event type and state. The same shape arrives on the realtime WebSocket and on webhooks.

Every event the gateway emits is one JSON object with the same outer fields. Those outer fields are the envelope; the part that changes per event type sits in one of them, payload. The gateway sends this same object two ways: on the realtime WebSocket and on webhooks (more on both below).

This page lists the envelope fields, then gives a complete, copy-pasteable example for every event type and every state it can be in. To set up a subscription, see Receive events.

Envelope fields

{
  "schema": "v1",
  "id": "evt_01J9XYZABCDEF0123456789",
  "event": "message",
  "session": "sess_01J8ABCDEF0123456789",
  "organization": "org_abc",
  "timestamp": 1719400000000,
  "payload": {}
}
FieldTypeWhat it is
schemastringEnvelope version. Always v1 today. If this ever changes, check it before reading the rest.
idstringThe event id: a ULID (a sortable unique id) prefixed with evt_. Use it to skip duplicates and to resume the stream where you left off.
eventstringThe event type, one of the values in the catalog below. It tells you how to read payload.
sessionstringThe session the event belongs to (sess_…). A session is one linked WhatsApp number.
organizationstringThe organization that owns the session. Every event belongs to exactly one organization.
timestampintegerWhen the gateway emitted the event, in milliseconds since the Unix epoch.
payloadobjectThe event-specific data. Its fields depend on event — see each entry below.

You get the same object whether you receive realtime events or webhooks, so you only write your handler once. The two delivery methods differ only in how the bytes reach you:

Realtime WebSocketWebhook
How it arrivesOpen a WebSocket after minting a single-use ticket via POST /api/v1/realtime/ticketThe gateway sends an HTTP POST to your URL
FormatDiscrete JSON text frames, one object per frameThe envelope as a JSON request body
KeepaliveA ping event arrives about every 20 secondsn/a
Dropped connectionMint a new ticket with since={lastEventId} to replay what you missedThe gateway retries the delivery
How to skip duplicatesTrack the envelope idThe envelope id is also in the X-Webhook-Request-Id header
AuthenticityThe ticket (single-use, short-lived) authorizes the socketEach POST is signed: X-Webhook-Hmac, with X-Webhook-Hmac-Algorithm: sha512

Connect to realtime events

Realtime events use a WebSocket with a two-step handshake: mint a ticket, then open the socket.

Step 1: Mint a ticket

curl -X POST "https://your-gateway/api/v1/realtime/ticket" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "scope": "organization",
    "events": ["*"],
    "session": "sess_01J8ABCDEF0123456789"
  }'

Response:

{
  "ticket": "rt_...",
  "expiresInSeconds": 30,
  "url": "wss://your-gateway/api/v1/realtime?ticket=rt_..."
}

Step 2: Open the WebSocket

Connect to the url from the response (or build it: take your base URL, swap http(s) for ws(s), and append /api/v1/realtime?ticket=rt_...).

The ticket is single-use and expires in ~30 seconds. Mint one ticket per connection, immediately before opening the socket. Use the ticket (not your bearer token) to authorize the connection.

Request body fields for the ticket

  • scope (string, optional, default organization) — organization (all sessions your org owns), session (one session), or firehose (all events).
  • session (string, optional) — required when scope is session. The sess_... id.
  • events (string array, optional, default ["*"]) — event types to receive. Examples: ["*"] for all, ["message", "poll.vote"] to narrow.
  • since (string, optional) — the id of the last event you processed. Use this when reconnecting to resume from where you left off. The server replays everything after this id (up to 1000 events), then tails live. Deduplicate on the id field.

Over the socket

The server sends discrete JSON text frames (not NDJSON lines):

  • First frame: {"event":"connected","heartbeatSeconds":20,"timestamp":...}
  • Heartbeat: {"event":"ping","timestamp":...} about every 20 seconds. If pings stop, reconnect.
  • Error: {"event":"error","error":"..."}
  • Data: the full event envelope with schema, id, event, session, organization, timestamp, payload.

Event catalog

The event types are grouped below. Every example is a complete envelope you can copy. A field that is empty is left out of the JSON entirely rather than sent as null (except media, which is explained below). Each entry lists which of its fields are optional or nullable.

EventFires when
session.statusA session changes connection state.
auth.qrA new QR code is ready to scan.
auth.codePairing succeeded.
messageAn inbound message arrives.
message.from_meA message is sent from this number, including from another device linked to it.
message.statusWhatsApp reports a receipt for one or more messages.
message.reactionSomeone reacts to a message.
message.interactive_replyA button or list option is selected.
message.editedA message is edited.
message.revokedA message is deleted for everyone.
poll.voteSomeone votes on a poll.
presence.updateA contact goes online/offline or starts typing.
group.updateA group's metadata changes.
group.participantSomeone joins, leaves, or changes role.
chat.updateA chat changes (currently profile-picture changes).
contact.updateA contact's name or details change.
call.incomingAn incoming call.
newsletter.updateA followed channel updates.

Attachment storage is configured per session. Linked sessions receive a tracked media.id; media.ready supplies the URL after upload. See attachment storage.

Session & Auth

Messages

The message and message.from_me events use the same payload fields; the type field tells you what kind of content it holds. hasMedia identifies attachments. media contains available metadata, a tracked id, and the URL once stored.

message

An inbound message. Pick a tab per content type.

A plain text message. A reply also carries quotedMessageId plus quoted-message context — quotedSenderJid/quotedSenderLid, quotedBody, and quotedFromMe — so you can resolve who and what was quoted without a REST round-trip. A message with mentions carries a JID-keyed mentions object with the mentioned user's pushName and per-group member tag when known (all shown here).

{
  "schema": "v1",
  "id": "evt_01J9MSGTEXT0000000000001",
  "event": "message",
  "session": "sess_01J8ABCDEF0123456789",
  "organization": "org_abc",
  "timestamp": 1719400010000,
  "payload": {
    "waMessageId": "3EB0A1B2C3D4E5F6A7B8",
    "chatJid": "120363012345678901@g.us",
    "senderJid": "6281234567890@s.whatsapp.net",
    "senderLid": "123456789012345@lid",
    "fromMe": false,
    "type": "text",
    "body": "@628999 are we still on for tomorrow?",
    "quotedMessageId": "3EB0FEDCBA9876543210",
    "quotedFromMe": false,
    "quotedSenderJid": "6289990001111@s.whatsapp.net",
    "quotedSenderLid": "998877665544332@lid",
    "quotedBody": "Yes, 3pm works for me.",
    "mentions": {
      "628999@s.whatsapp.net": {
        "pushName": "Sam",
        "tag": "Sam Ops"
      }
    },
    "hasMedia": false,
    "media": null,
    "timestamp": 1719400010000,
    "pushName": "Alex"
  }
}

An image, video, audio clip, document, or sticker. type says which one, hasMedia is true. Stored attachments receive media.id and a URL after upload. If the message has a caption, it is in body.

{
  "schema": "v1",
  "id": "evt_01J9MSGMEDIA000000000001",
  "event": "message",
  "session": "sess_01J8ABCDEF0123456789",
  "organization": "org_abc",
  "timestamp": 1719400011000,
  "payload": {
    "waMessageId": "3EB0A1B2C3D4E5F6A7B9",
    "chatJid": "6281234567890@s.whatsapp.net",
    "senderJid": "6281234567890@s.whatsapp.net",
    "fromMe": false,
    "type": "image",
    "body": "here's the receipt",
    "hasMedia": true,
    "media": null,
    "timestamp": 1719400011000,
    "pushName": "Alex"
  }
}

type is one of image, video, audio, document, sticker. Link the session to a bucket to capture files. media.ready reports completed uploads.

A shared location. The coordinates ride in a location object.

{
  "schema": "v1",
  "id": "evt_01J9MSGLOC00000000000001",
  "event": "message",
  "session": "sess_01J8ABCDEF0123456789",
  "organization": "org_abc",
  "timestamp": 1719400012000,
  "payload": {
    "waMessageId": "3EB0A1B2C3D4E5F6A7BA",
    "chatJid": "6281234567890@s.whatsapp.net",
    "senderJid": "6281234567890@s.whatsapp.net",
    "fromMe": false,
    "type": "location",
    "hasMedia": false,
    "media": null,
    "location": {
      "latitude": -6.200000,
      "longitude": 106.816666,
      "name": "Monas",
      "address": "Gambir, Jakarta Pusat"
    },
    "timestamp": 1719400012000,
    "pushName": "Alex"
  }
}

location.name and location.address are optional; latitude/longitude are always present.

A shared contact card. The raw vCard is kept verbatim in contact.vcard.

{
  "schema": "v1",
  "id": "evt_01J9MSGCONTACT00000000001",
  "event": "message",
  "session": "sess_01J8ABCDEF0123456789",
  "organization": "org_abc",
  "timestamp": 1719400013000,
  "payload": {
    "waMessageId": "3EB0A1B2C3D4E5F6A7BB",
    "chatJid": "6281234567890@s.whatsapp.net",
    "senderJid": "6281234567890@s.whatsapp.net",
    "fromMe": false,
    "type": "contact",
    "body": "Jamie Rivera",
    "hasMedia": false,
    "media": null,
    "contact": {
      "displayName": "Jamie Rivera",
      "vcard": "BEGIN:VCARD\nVERSION:3.0\nFN:Jamie Rivera\nTEL;type=CELL;waid=628111222333:+62 811-222-333\nEND:VCARD"
    },
    "timestamp": 1719400013000,
    "pushName": "Alex"
  }
}

contact.displayName and contact.vcard are both optional. body mirrors the display name.

A poll-creation message. The poll definition rides in a poll object. Votes arrive separately as poll.vote.

{
  "schema": "v1",
  "id": "evt_01J9MSGPOLL0000000000001",
  "event": "message",
  "session": "sess_01J8ABCDEF0123456789",
  "organization": "org_abc",
  "timestamp": 1719400014000,
  "payload": {
    "waMessageId": "3EB0A1B2C3D4E5F6A7BC",
    "chatJid": "120363012345678901@g.us",
    "senderJid": "6281234567890@s.whatsapp.net",
    "fromMe": false,
    "type": "poll",
    "body": "Lunch on Friday?",
    "hasMedia": false,
    "media": null,
    "poll": {
      "name": "Lunch on Friday?",
      "options": ["Pizza", "Sushi", "Salad"],
      "selectableCount": 1
    },
    "timestamp": 1719400014000,
    "pushName": "Alex"
  }
}

poll.selectableCount is the max number of options a voter may pick. body mirrors poll.name.

Common fields (every message / message.from_me payload)

  • waMessageId (string, required) — the WhatsApp message id; the target other events reference.
  • chatJid (string, required) — the chat (DM @s.whatsapp.net, group @g.us, etc.).
  • senderJid (string, optional) — phone-number JID of the sender.
  • senderLid (string, optional) — sender's LID identity, when the addressing uses it.
  • fromMe (boolean, required) — false for message; true for message.from_me.
  • type (string, required) — text, image, video, audio, document, sticker, location, contact, poll.
  • body (string, optional) — text / caption / poll name / contact display name.
  • quotedMessageId (string, optional) — set when the message is a reply.
  • quotedFromMe (boolean, optional) — true when the quoted message was sent by this account. Best-effort: resolved from the locally stored quoted message, so false if the quote predates local retention.
  • quotedSenderJid / quotedSenderLid (string, optional) — the quoted message's author. Guaranteed from the reply's context for a genuine reply; else resolved from local storage.
  • quotedBody (string, optional) — the quoted message's text/caption (truncated to 4096 bytes). Guaranteed from the reply's context for text/caption replies; else the locally stored body.
  • mentions (object, optional) — mentioned JIDs keyed to { pushName, tag } metadata.
  • hasMedia (boolean, required) — true when an attachment exists.
  • media (object|null) — available attachment metadata, storage id, URL, and optional retention deadline.
  • timestamp (integer, required) — epoch-ms.
  • pushName (string, optional) — the sender's display name.

message.from_me

Same fields as message. You get this when a message was sent from your own number, including from another device linked to it. The only difference is that fromMe is true.

{
  "schema": "v1",
  "id": "evt_01J9MSGFROMME00000000001",
  "event": "message.from_me",
  "session": "sess_01J8ABCDEF0123456789",
  "organization": "org_abc",
  "timestamp": 1719400015000,
  "payload": {
    "waMessageId": "3EB0A1B2C3D4E5F6A7BD",
    "chatJid": "6281234567890@s.whatsapp.net",
    "senderJid": "6289876543210@s.whatsapp.net",
    "fromMe": true,
    "type": "text",
    "body": "yes, see you at noon",
    "hasMedia": false,
    "media": null,
    "timestamp": 1719400015000,
    "pushName": "You"
  }
}

message.status

WhatsApp reported a lifecycle receipt for one or more messages. A single receipt can cover several messages at once, so messageIds is a list. The list may contain an ID the gateway never captured (for example, old history or a message sent from another linked device). Consumers should only advance their known status and ignore unknown or stale IDs.

The status value comes from one of two places:

StatusSet by
pending, sent, failedThe gateway, as it accepts and sends your outbound message. Not from WhatsApp.
delivered, read, playedA receipt from WhatsApp.

Queued by the gateway, not yet acknowledged by WhatsApp (ack level 0). Set on send, not from a receipt.

{
  "schema": "v1",
  "id": "evt_01J9STPENDING00000000001",
  "event": "message.status",
  "session": "sess_01J8ABCDEF0123456789",
  "organization": "org_abc",
  "timestamp": 1719400016000,
  "payload": {
    "chatJid": "6281234567890@s.whatsapp.net",
    "messageIds": ["3EB0A1B2C3D4E5F6A7BD"],
    "status": "pending",
    "timestamp": 1719400016000
  }
}

Accepted by the WhatsApp server (ack level 1). Set on send, not from a receipt.

{
  "schema": "v1",
  "id": "evt_01J9STSENT000000000000001",
  "event": "message.status",
  "session": "sess_01J8ABCDEF0123456789",
  "organization": "org_abc",
  "timestamp": 1719400016500,
  "payload": {
    "chatJid": "6281234567890@s.whatsapp.net",
    "messageIds": ["3EB0A1B2C3D4E5F6A7BD"],
    "status": "sent",
    "timestamp": 1719400016500
  }
}

Delivered to the recipient's device (ack level 2). Driven by a delivery receipt.

{
  "schema": "v1",
  "id": "evt_01J9STDELIVERED000000001",
  "event": "message.status",
  "session": "sess_01J8ABCDEF0123456789",
  "organization": "org_abc",
  "timestamp": 1719400017000,
  "payload": {
    "chatJid": "6281234567890@s.whatsapp.net",
    "senderJid": "6281234567890@s.whatsapp.net",
    "messageIds": ["3EB0A1B2C3D4E5F6A7BD"],
    "status": "delivered",
    "timestamp": 1719400017000
  }
}

Read by the recipient (ack level 3, the blue ticks). Driven by a read receipt.

{
  "schema": "v1",
  "id": "evt_01J9STREAD0000000000001",
  "event": "message.status",
  "session": "sess_01J8ABCDEF0123456789",
  "organization": "org_abc",
  "timestamp": 1719400018000,
  "payload": {
    "chatJid": "6281234567890@s.whatsapp.net",
    "senderJid": "6281234567890@s.whatsapp.net",
    "messageIds": ["3EB0A1B2C3D4E5F6A7BD", "3EB0A1B2C3D4E5F6A7BE"],
    "status": "read",
    "timestamp": 1719400018000
  }
}

A voice note / view-once media was played (ack level 4). Driven by a played receipt.

{
  "schema": "v1",
  "id": "evt_01J9STPLAYED000000000001",
  "event": "message.status",
  "session": "sess_01J8ABCDEF0123456789",
  "organization": "org_abc",
  "timestamp": 1719400019000,
  "payload": {
    "chatJid": "6281234567890@s.whatsapp.net",
    "senderJid": "6281234567890@s.whatsapp.net",
    "messageIds": ["3EB0A1B2C3D4E5F6A7BF"],
    "status": "played",
    "timestamp": 1719400019000
  }
}

The message failed to send. Set internally, not from a receipt.

{
  "schema": "v1",
  "id": "evt_01J9STFAILED00000000001",
  "event": "message.status",
  "session": "sess_01J8ABCDEF0123456789",
  "organization": "org_abc",
  "timestamp": 1719400019500,
  "payload": {
    "chatJid": "6281234567890@s.whatsapp.net",
    "messageIds": ["3EB0A1B2C3D4E5F6A7C0"],
    "status": "failed",
    "timestamp": 1719400019500
  }
}

Fields

  • chatJid (string, required) — the chat the message belongs to.
  • senderJid (string, optional) — the JID the receipt came from (set on receipt-driven updates).
  • messageIds (string[], required) — the waMessageIds referenced by this receipt; some may be unknown locally.
  • status (string, required) — pending, sent, delivered, read, played, failed.
  • timestamp (integer, required) — epoch-ms.

Reactions, edits, and deletes

These three are their own event types, but they reuse the message payload fields. In all three, targetId is the waMessageId of the message being acted on.

Polls

Poll creation

A poll being posted arrives as a regular message event (or message.from_me if this account posted it) with type set to poll. The question and options are in the poll field. The gateway records the options so it can resolve later votes back to readable labels.

{
  "schema": "v1",
  "id": "evt_01J9POLLNEW0000000000001",
  "event": "message",
  "session": "sess_01J8ABCDEF0123456789",
  "organization": "org_abc",
  "timestamp": 1719400023500,
  "payload": {
    "waMessageId": "3EB0A1B2C3D4E5F6A7BC",
    "chatJid": "120363012345678901@g.us",
    "senderJid": "6281234567890@s.whatsapp.net",
    "fromMe": false,
    "type": "poll",
    "body": "Lunch?",
    "poll": {
      "name": "Lunch?",
      "options": ["Pizza", "Sushi"],
      "selectableCount": 1
    },
    "hasMedia": false,
    "media": null,
    "timestamp": 1719400023500,
    "pushName": "Alex"
  }
}

Fields

  • poll.name (string) — the poll question.
  • poll.options (string[]) — the answer options, in creation order.
  • poll.selectableCount (number) — how many options a voter may pick (1 = single choice).

poll.vote

Someone voted on a poll. targetId is the original poll message they voted on. The production normalizer currently emits an empty selectedOptions array: poll-vote decryption and stored-option lookup are not wired into its runtime. The event identifies the poll and voter; selected labels remain unavailable. The adapter supports resolving decrypted SHA-256 hashes to stored labels when those dependencies are supplied, with unresolved hashes retained as strings.

{
  "schema": "v1",
  "id": "evt_01J9POLLVOTE0000000000001",
  "event": "poll.vote",
  "session": "sess_01J8ABCDEF0123456789",
  "organization": "org_abc",
  "timestamp": 1719400024000,
  "payload": {
    "waMessageId": "3EB0A1B2C3D4E5F6A7C5",
    "chatJid": "120363012345678901@g.us",
    "senderJid": "6281234567890@s.whatsapp.net",
    "fromMe": false,
    "type": "poll_vote",
    "targetId": "3EB0A1B2C3D4E5F6A7BC",
    "selectedOptions": [],
    "hasMedia": false,
    "media": null,
    "timestamp": 1719400024000,
    "pushName": "Alex"
  }
}

Fields

  • targetId (string) — the poll-creation message's waMessageId.
  • selectedOptions (string[]) — empty in the current production runtime; populated by adapters configured with vote decryption and option lookup.
  • Plus the common message fields (waMessageId, chatJid, senderJid, fromMe, timestamp, pushName).

Presence, chats & contacts

Groups

Calls & newsletters

For the endpoints that produce and receive these events, see the API reference.

Interactive replies

message.interactive_reply carries the normal message payload with type: "interactive_reply". Its interactiveReply object contains kind (button or list), the selected id, and title. quotedMessageId identifies the original message when WhatsApp provides it. The chat and sender identify who selected the option, including in a group chat. Selections use this event instead of the ordinary message event. See sending interactive messages for a payload example and compatibility limitations.

Media storage events

media.ready reports a stored attachment and its access URL. media.expired reports completed retention cleanup. Both include the session, WhatsApp message id, attachment id, connection id, and album item index. Read attachment storage for their fields and expiry behavior.

On this page