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:
| Way | How it works | Use it when |
|---|---|---|
| Realtime WebSocket | You 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. |
| Webhooks | The 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": { }
}| Field | What it is |
|---|---|
schema | Envelope version. Currently v1. |
id | Unique id for this event. Use it to drop duplicates (see webhooks below). |
event | The event type — one of the names in the catalog below. |
session | The session the event belongs to. |
organization | The organization that owns the session. |
timestamp | When it happened, in epoch milliseconds (milliseconds since 1 Jan 1970). |
payload | The 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:
| Field | Effect |
|---|---|
scope | One 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). |
session | The session id. Required when scope is session. |
events | Array of event types to receive: ["*"] for all, or specific names like ["message","poll.vote"]. Default is ["*"]. |
since | The 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:
| Setting | What it does |
|---|---|
| URL | Where to send the POST. |
| events | Which event types to send: ["*"] for all, or a list like ["message","poll.vote"]. An empty list matches nothing. |
| session | Optional. Limit to one session. Leave it off to cover all the organization's sessions. |
| custom headers | Optional. Extra headers added to every delivery (applied last, so they can override the defaults below). |
| retry policy | Optional. How many times and how often to retry a failed delivery. |
Delivery headers
Every POST carries these headers:
| Header | Purpose |
|---|---|
Content-Type: application/json | The body is the JSON event envelope. |
X-Webhook-Request-Id | The event id. Use it to drop duplicate deliveries. |
X-Webhook-Timestamp | When the gateway sent this delivery (epoch milliseconds). |
X-Webhook-Hmac | Signature of the body. Present only when the webhook has a secret. |
X-Webhook-Hmac-Algorithm | The 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.
| Event | Fires when |
|---|---|
session.status | A session changes status (for example working, logged_out). |
auth.qr | A new QR code is available to scan. |
auth.code | A pairing code is available. |
message | A message comes in. |
message.from_me | A message was sent from this number, including from another linked device. |
message.status | A delivery receipt updates a sent message (sent, then delivered, then read). |
message.reaction | Someone reacts to a message. |
message.edited | A message is edited. |
message.revoked | A message is deleted for everyone. |
poll.vote | Someone votes on a poll. |
presence.update | A contact goes online or offline, or starts typing. |
group.update | A group's metadata changes (name, settings). |
group.participant | Someone joins, leaves, or changes role in a group. |
chat.update | A chat changes (archived, pinned, read state). |
contact.update | A contact's name or details change. |
call.incoming | A call comes in. |
newsletter.update | A 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.