Push stream
Receive events over a WebSocket, with replay from the last event you processed.
The push stream delivers the same events as webhooks over one WebSocket you open from your server. It needs no public URL, so it suits workers behind a firewall and local development. Use it next to webhooks or instead of them.
wss://api.whabuzz.com/v1/stream?since=<seq> is a WebSocket endpoint, not a REST endpoint, so it is not an operation in the OpenAPI file.
#Connect
Authenticate with your API key in the Sec-WebSocket-Protocol header: offer whabuzz.v1 and then the key. This works in browsers and in the standard WebSocket of Node 22 and later, which cannot set an Authorization header. Server-side clients that can set headers may send Authorization: Bearer wb_live_... instead.
const key = process.env.WHABUZZ_API_KEY
let since = 0 // persist this between runs
function connect() {
const ws = new WebSocket(`wss://api.whabuzz.com/v1/stream?since=${since}`, ['whabuzz.v1', key])
ws.onmessage = (m) => {
const frame = JSON.parse(m.data)
if (frame.type === 'stream.replay_truncated') return // the socket closes next; reconnect from since
handle(frame) // your code; dedupe on frame.dedupe_key
since = frame.seq
}
ws.onclose = (e) => {
if (e.code === 4429) return console.error('too many open streams for this tenant')
setTimeout(connect, 1000) // reconnect; since resumes where you stopped
}
setInterval(() => ws.readyState === 1 && ws.send('ping'), 25_000) // answered with "pong"
}
connect()js#Frames
Each frame is one JSON text message: the exact webhook envelope plus seq, the event's position in your tenant's own log. seq only grows. Frames arrive in seq order, with no duplicates within one connection. Delivery is at least once across reconnects, so keep deduplicating on dedupe_key.
#Resume with since
Pass since=<seq> (the seq of the last frame you processed) to receive everything after it, oldest first, and then live events on the same socket. With no since, you get live events only. A since beyond the newest event is treated as the newest event.
A single connection replays at most 10,000 events. If you are further behind, the stream sends {"type":"stream.replay_truncated","last_seq":N} and closes with code 1000. Reconnect with since=N and carry on.
Replayed frames are rebuilt from your event log, so display_phone_number can be null and raw can be null when the raw body has moved to archive storage. Live frames carry the full envelope. Fetch the event by id from the events API when you need the archived body.
#Limits and close codes
- 10 open streams per tenant. The 11th is accepted and closed at once with code 4429 (
stream_cap). Close one of your own streams, then reconnect. - Frames above 1 MB drop the
rawfield. - Idle sockets cost nothing on our side. Send
pingas a text message to getpong; there is no server ping.
| Code | Meaning |
|---|---|
| 1000 | Normal close, including replay_truncated. Reconnect with since. |
| 4429 | Tenant is at its 10-stream cap. |
Before the socket opens, errors are ordinary HTTP responses: 401 for a bad key, 403 for a suspended tenant, 400 for a bad since, 426 when the request is not a WebSocket upgrade.
#Sandbox
With a wb_test_ key, an open stream counts as a connected webhook endpoint, so the tenant shows as connected without a public URL. Live keys are unchanged: a live tenant still needs a webhook endpoint to count as connected.
This page as Markdown: /stream.md. All docs in one file: /llms-full.txt.