WhatsApp Gateway
Guides

Receive events

Read gateway events via WebSocket realtime stream, or have the gateway POST each one to a URL you run.

Whenever something happens on one of your sessions — a message arrives, a delivery receipt comes back, someone votes on a poll, a session changes status — the gateway produces an event. You read those events in one of two ways:

WayHow it worksUse it when
Realtime WebSocketYou mint a ticket, open a WebSocket connection, and receive events as discrete JSON frames as they happen.Your code can hold a connection open — a worker, a script, the dashboard.
WebhooksThe gateway sends an HTTP POST to a URL you run, once per event, and retries until it succeeds.You have a public URL and would rather not hold a connection open.

Both carry the same event envelope (the JSON wrapper described below), so the data you receive is identical whichever you pick.

The event envelope

Every event is one JSON object with the same outer fields:

{
  "schema": "v1",
  "id": "evt_01J9...",
  "event": "message",
  "session": "sess_01J8...",
  "organization": "org_abc",
  "timestamp": 1719400000000,
  "payload": { }
}
FieldWhat it is
schemaEnvelope version. Currently v1.
idUnique id for this event. Use it to drop duplicates (see webhooks below).
eventThe event type — one of the names in the catalog below.
sessionThe session the event belongs to.
organizationThe organization that owns the session.
timestampWhen it happened, in epoch milliseconds (milliseconds since 1 Jan 1970).
payloadThe type-specific data. Its shape depends on event.

For every field of every payload type, with concrete examples, see Event shape.

Stream the events via WebSocket

The gateway delivers events over a WebSocket connection. The connection uses a two-step handshake: first you mint a single-use ticket, then you connect with that ticket.

Step 1: Mint a ticket

Send a POST request to /api/v1/realtime/ticket with your Bearer token. The request body is optional JSON:

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

The request body fields are all optional:

FieldEffect
scopeOne of session, organization, or firehose. Default is organization (all sessions your org owns). Use session to subscribe to one session only (then session is required).
sessionThe session id. Required when scope is session.
eventsArray of event types to receive: ["*"] for all, or specific names like ["message","poll.vote"]. Default is ["*"].
sinceThe id of the last event you processed (for resume/replay after a reconnect). Omit or leave empty to get live events only.

The response is JSON:

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

The ticket is single-use and expires in about 30 seconds. Mint one ticket per connection, immediately before connecting. The ticket (not your bearer token) authorizes the WebSocket.

Step 2: Open the WebSocket

Connect to the URL returned in the ticket response (or build it: take your gateway base URL, swap http:// for ws:// and https:// for wss://, then append /api/v1/realtime?ticket=rt_...).

Over the socket the server sends discrete JSON text frames — one JSON object per frame:

  • First frame: {"event":"connected","heartbeatSeconds":20,"timestamp":<epoch-ms>} — the connection is ready.
  • Heartbeat: {"event":"ping","timestamp":<epoch-ms>} — sent about every 20 seconds. If pings stop, the connection is dead; reconnect.
  • Error: {"event":"error","error":"<error message>"} — something went wrong.
  • Data frame: The full event envelope (schema, id, event, session, organization, timestamp, payload).

Reconnect after a drop

To resume after a disconnect and get the events you missed, mint a new ticket with since set to the id of the last event you processed. The server replays everything after that id from its durable log (up to 1000 events), then resumes tailing live events. Replayed events are identical to live ones, so you must deduplicate on the event id.

A login JWT expires after about 5 minutes, but the ticket carries the connection — your token only needs to be valid when you mint the ticket. Each reconnect mints a fresh ticket with a new token, so expiry never interrupts your stream as long as you reconnect with since.

Use webhooks instead

Register a webhook and the gateway delivers each matching event to your URL as a JSON POST. You create and manage webhooks through the gateway API (create a webhook); the dashboard shows the same config read-only.

A webhook is configured with:

SettingWhat it does
URLWhere to send the POST.
eventsWhich event types to send: ["*"] for all, or a list like ["message","poll.vote"]. An empty list matches nothing.
sessionOptional. Limit to one session. Leave it off to cover all the organization's sessions.
custom headersOptional. Extra headers added to every delivery (applied last, so they can override the defaults below).
retry policyOptional. How many times and how often to retry a failed delivery.

Delivery headers

Every POST carries these headers:

HeaderPurpose
Content-Type: application/jsonThe body is the JSON event envelope.
X-Webhook-Request-IdThe event id. Use it to drop duplicate deliveries.
X-Webhook-TimestampWhen the gateway sent this delivery (epoch milliseconds).
X-Webhook-HmacSignature of the body. Present only when the webhook has a secret.
X-Webhook-Hmac-AlgorithmThe signature algorithm. Always sha512.

Before you trust a delivery

  • Check the signature. When the webhook has a secret, compute the HMAC-SHA512 of the exact raw request body using your secret, encode it as lowercase hex, and compare it to X-Webhook-Hmac. If they differ, reject the request. (HMAC is a keyed hash: only someone holding the secret can produce a matching value, so a match proves the body came from the gateway and was not altered.)
  • Drop duplicates. A retried delivery repeats the same event id (X-Webhook-Request-Id). Keep track of ids you have already handled and skip repeats.

The gateway retries a failed delivery with growing gaps between attempts. Once a delivery is accepted (a 2xx response) it stops; once it runs out of retry attempts it is marked dead and not tried again.

Event catalog

Every event type the gateway emits. Set events to ["*"] (the default) to receive all of them, or list just the ones you want.

EventFires when
session.statusA session changes status (for example working, logged_out).
auth.qrA new QR code is available to scan.
auth.codeA pairing code is available.
messageA message comes in.
message.from_meA message was sent from this number, including from another linked device.
message.statusA delivery receipt updates a sent message (sent, then delivered, then read).
message.reactionSomeone reacts to a message.
message.editedA message is edited.
message.revokedA message is deleted for everyone.
poll.voteSomeone votes on a poll.
presence.updateA contact goes online or offline, or starts typing.
group.updateA group's metadata changes (name, settings).
group.participantSomeone joins, leaves, or changes role in a group.
chat.updateA chat changes (archived, pinned, read state).
contact.updateA contact's name or details change.
call.incomingA call comes in.
newsletter.updateA followed channel updates.

Media is metadata only. A message with an attachment arrives with hasMedia: true and media: null — you see that it has media and what type it is, but this release does not download the file.

On this page