Skip to content
WhatIsUp.dev
Esta página está disponible solo en inglés por ahora.

Events (SSE)

A single-direction Server-Sent Events stream for live state. Use it instead of polling for QR codes, channel state changes, and message activity.

Endpoint

GET/v1/eventsBearer · API key

Authenticate via Authorization: Bearer … or ?token=… query string. (Browser EventSource can't set headers; the query-token path is for that case.)

SSE stream — keep the connection open. Most languages have a streaming HTTP client; the snippets below assume the simplest one available per ecosystem.
curl -s "$WHATISUP_API/v1/events" \
  -H "Authorization: Bearer $WHATISUP_API_KEY"

For browser code specifically, EventSource handles reconnection for you. Every frame carries a named SSE event, so addEventListener('message', …) will never fire — listen per name, or use onmessage's named-event equivalent:

const es = new EventSource(`${WHATISUP_API}/v1/events?token=${WHATISUP_API_KEY}`);
 
for (const name of [
  'channel.status_changed', 'channel.qr', 'channel.connected',
  'channel.disconnected', 'channel.needs_attention',
  'message.received', 'message.sent', 'message.status',
  'contact.resolved',
]) {
  es.addEventListener(name, (e) => {
    const data = JSON.parse(e.data);
    console.log(name, data.channel_id, data);
  });
}
 
// Heartbeat. Ignore it, or use it as a liveness check.
es.addEventListener('ping', () => {});
es.onerror = () => { /* EventSource retries on its own */ };

Filtering

There is no server-side filtering. /v1/events accepts no filter query params — a channel_id= or events= you pass is ignored, not honoured. Filter client-side on the frame name and data.channel_id.

Two things do narrow the stream for you, both implicit:

NarrowingHow
Account scopeYou only ever receive events for your own customer_id.
Channel scopeA channel-bound API key receives only that channel's events.

If you want one channel and hold an account-wide key, issue a channel-bound key (POST /v1/channels with issue_scoped_key: true) and stream with that.

Frame format

Standard SSE, with a named event and an id on every frame:

: stream-open

event: channel.qr
id: 1
data: {"channel_id":"8d653c66-e4ff-43ee-97da-3de5ad5680d4","qr_png_base64":"iVBORw0KGgo...","expires_at":1779425257573}

event: channel.connected
id: 2
data: {"channel_id":"8d653c66-e4ff-43ee-97da-3de5ad5680d4","phone_number":"5511999999999"}

event: ping
id: 3
data: {"ts":1779425282573}
  • : stream-open is a one-shot comment sent when the stream opens.
  • The keepalive is a real named ping event every 25 seconds — not a comment line. Most reverse proxies idle-close at 30s.
  • id: is a per-connection counter starting at 1. It is not a resume cursor: Last-Event-ID is not honoured, and reconnecting restarts at 1.

What's on the wire

The SSE vocabulary is not identical to the webhook vocabulary. Three names exist only here, several webhook events never appear here, and qr.updated (webhooks) is called channel.qr on SSE. Do not write one handler keyed on webhook names and point it at this stream.

SSE eventMeaningWebhook equivalent
channel.status_changedLifecycle state moved. Carries status.channel.status
channel.qrNew QR available (qr_png_base64, expires_at).qr.updated
channel.connectedPairing complete.channel.connected
channel.disconnectedSession ended (with reason).channel.disconnected
channel.needs_attentionChannel is stuck and needs a human (reason, at).(SSE only)
message.receivedInbound from WhatsApp.message.received
message.sentOutbound handed to WhatsApp.message.sent
message.statusDelivery state moved (sent/delivered/read/played/failed).message.status
group.created, group.updated, group.participant_added, group.participant_removed, group.admin_promoted, group.admin_demotedGroup changes.same names
contact.resolvedA LID-only contact resolved to a phone JID.contact.resolved
pingKeepalive every 25s.(SSE only)

There is no message.delivered and no message.failed on this stream — both are message.status frames discriminated by the status field. Chat, order, cart, story, call and presence events are webhook-only; they do not appear on SSE.

Every frame's data object carries channel_id at the top level, then the event payload flattened alongside it — there is no envelope/data nesting and no signature. The payload shapes match Webhooks → Event payloads; the wrapper does not.

Limits

  • 10 concurrent streams per account. The 11th gets 429 with code: "sse_connection_limit".
  • Opening a stream costs one rate-limit token like any other authenticated call.
  • No replay buffer: events emitted while you are disconnected are lost. Use webhooks for anything that must not be missed.

When to use SSE vs webhooks

WantUse
First-party app: dashboard, internal tool, your own UISSE — no public webhook to maintain
Third-party: someone else's HTTPS endpointWebhooks — durable, retried, audited
BothBoth. They don't conflict.

Webhooks are the durable record (retried, logged, replayable). SSE is the live feed (best-effort, no buffering — if you disconnect mid-event, you lose it).