Skip to content

Realtime stream

The stream pushes changes as they happen over one WebSocket: wss://api.status302.com/v1/stream. The Status302 app runs on this same stream, so what moves on screen moves for you at the same time. One connection carries many topic subscriptions; every message of a topic has a sequence number, so you can reconnect and resume without losing or repeating anything.

The message schemas and every code are in the protocol reference, generated from the AsyncAPI document.

There are two ways to authenticate the upgrade:

From How Notes
Your servers Authorization: Bearer <key> on the upgrade request The key needs the stream scope to connect, and each topic also needs the scope of its REST equivalent (see topics).
A browser ?ticket=<ticket> from POST /v1/stream/ticket Browsers can’t set headers on a WebSocket. Your backend gets the ticket with its key; the page never sees the key.

A ticket is single-use and valid for 60 seconds; get a new one for every connection (the SDKs ask for one before each reconnect). A connection opened with a ticket acts as the key that issued it, with the same scopes.

samples/curl/stream-ticket.sh
#!/usr/bin/env bash
# A single-use stream ticket (valid 60 s) for a browser: your backend calls this
# with its key and hands the ticket to the page, which connects to
# wss://api.status302.com/v1/stream?ticket=<ticket>. The key never reaches the browser.
set -euo pipefail
API="${STATUS302_API_URL:-https://api.status302.com}"
curl --fail-with-body -sS -X POST "$API/v1/stream/ticket" \
-H "Authorization: Bearer $STATUS302_API_KEY"

An invalid, revoked or expired key on the upgrade answers HTTP 401 with an invalid-api-key problem before the WebSocket opens. Credentials that stop being valid while the connection is open close it with code 4401.

Send JSON text frames. Each request carries an id of your choosing, which the reply echoes:

{"op": "subscribe", "id": "r1", "topic": "event.4102.sales"}

The server confirms with the topic’s current head seq and the REST snapshot path that returns the topic’s full current state:

{"type": "subscribed", "id": "r1", "topic": "event.4102.sales", "seq": 48230, "snapshot": "/v1/events/4102/sales"}

Then, for every change:

{"type": "data", "topic": "event.4102.sales", "seq": 48231, "ts": "2026-10-28T18:04:11.204Z",
"data": {"eventId": 4102, "scope": "primary", "kind": "sale", "section": "108", "row": "C", "qty": 2,
"priceMinor": 18450, "listMinor": 15500, "currency": "USD", "confidence": "inferred_high"}}

The usual pattern: subscribe, load the snapshot, then apply data messages with a seq above the head seq you got in subscribed.

  • {"op": "unsubscribe", "id": "r2", "topic": "event.4102.sales"} ends a subscription.
  • Subscribing to a topic you already have replaces its filter.
  • {"op": "ping", "id": "r3"} is answered by a heartbeat echoing the id. The server also sends a heartbeat every 20 seconds.
  • Refused requests answer {"type": "error", "id": "r1", "code": "forbidden_topic", "message": "…"}; the connection stays open. A topic of an event id that doesn’t exist answers unknown_topic with unknown event <id>.
samples/typescript/stream.ts
// Your organization's alert firings, live. The client reconnects on its own,
// resumes after the last seq it delivered, and drops duplicates.
import { Status302, type Schema } from "@status302/sdk";
const status302 = new Status302({ apiKey: process.env.STATUS302_API_KEY, baseUrl: process.env.STATUS302_API_URL });
const stream = status302.stream({
onStateChange: (state) => console.log(`stream ${state}`),
onError: (err) => console.error(`stream error: ${err.message}`),
});
const alerts = stream.subscribe<Schema<"AlertEvent">>("alerts", {
onData: (msg) => console.log(`alert #${msg.seq} at ${msg.ts}: ${msg.data.title}`),
// The gap could not be replayed: reload the REST snapshot, then continue live.
onResync: async (msg) => {
const snapshot = await alerts.fetchSnapshot(msg);
console.log(`resynced ${msg.topic} (${msg.reason}) from ${msg.snapshot}`, snapshot);
},
onError: (err) => console.error(`subscription refused: ${err.code}`),
});
console.log(`subscribed to alerts at seq ${await alerts.ready}`);
process.on("SIGINT", () => stream.close());
Topic Payload Delivery REST snapshot API key scope
event.{id}.kpis The event’s metrics per market Coalesced: latest state, at most 1 per second GET /v1/events/{id}/metrics market:read
event.{id}.sales Each new sale of the event Every message, replayable GET /v1/events/{id}/sales sales:read
event.{id}.primary Each Ticketmaster inventory change batch: restocks, drops, released holds, sales Every message, replayable GET /v1/events/{id}/inventory/changes maps:read or market:read
sales Every sale of every event (the firehose), filtered Every message, replayable GET /v1/sales/feed sales:read
onsales Newly announced events with their on-sale and presale dates Every message, replayable GET /v1/events?sort=firstSeen catalog:read
alerts Your organization’s alert firings Every message, replayable GET /v1/alerts/events alerts:write

A key without a topic’s scope gets a forbidden_topic error naming it, for example this API key lacks the "sales:read" scope (event.{id}.primary: this API key lacks the "maps:read" or "market:read" scope). The Read + stream preset covers every topic but alerts, which needs alerts:write.

event.{id}.listings, event.{id}.map and catalog.movers are reserved for upcoming producers and answer unknown_topic today.

Pass filter with the subscribe. Filters use the same names and defaults as the topic’s REST snapshot:

{"op": "subscribe", "id": "r1", "topic": "sales",
"filter": {"country": ["US", "CA"], "segment": ["Music"], "minPrice": 10000, "confidence": ["inferred_high"]}}
Topic Filters
sales, event.{id}.sales country, segment, minPrice (minor units), scope (primary, tm_resale), confidence (the firehose defaults to ["inferred_high"])
sales only eventIds (up to 500) or listId (one of your saved lists; membership refreshes every minute)
onsales country, segment, attractionIds (up to 100)
Others None

The sales and onsales firehoses need the Elite plan or higher. Filter them as narrowly as you can: you receive, and must keep up with, every message that matches.

Connections drop: deploys, networks, your own restarts. Replayable topics keep the last 15 minutes of messages. To resume, subscribe again with since set to the last seq you processed:

{"op": "subscribe", "id": "r1", "topic": "event.4102.sales", "since": 48231}

The server replays every message after that seq, then continues live. Delivery is at least once: around a reconnect you can receive a message twice, so drop any message whose seq is not above the last one you processed. Within a topic, seqs only increase.

If the gap is older than the replay window (or older than your plan’s sales history), the server can’t replay it and sends resync instead:

{"type": "resync", "topic": "event.4102.sales", "reason": "gap", "snapshot": "/v1/events/4102/sales"}

Reload the state from snapshot with a REST request, then keep applying live messages. reason is gap for a gap beyond the window and backpressure when so many live messages arrived during a long replay that the server couldn’t hold them until the replay finished.

A topic’s seq can also start over (after the server lost its counters, for example a cache restart): the server then sends a gap resync on every topic it affects, coalesced ones included, and continues from the new seq. So after any resync, forget the last seq you kept for that topic: the next messages may have lower seqs and are not duplicates.

Coalesced topics (event.{id}.kpis) only ever carry the latest state, so since doesn’t apply: after a reconnect, reload the snapshot or wait for the next message.

Connections end with a WebSocket close code. Reconnect with backoff for the retryable ones; the others need new credentials or a plan change first.

CodeNameMeaningReconnect?
1001going_awayIdle timeout (no client message or pong for 60 s; the server sends heartbeat and pings every 20 s), or a failed write.Yes, with backoff; resume with since
1009message_too_bigA client message exceeded 64 KiB.Yes, with backoff; resume with since
1012service_restartThe server is restarting; reconnect and resume with since.Yes, with backoff; resume with since
4008slow_consumerThe connection's send buffer overflowed; reconnect and resume with since.Yes, with backoff; resume with since
4401unauthorizedNo credentials, ticket invalid or already used, session or API key revoked, key expired or past its rotation grace. Get new credentials before reconnecting.No, not with the same credentials
4403forbiddenThe plan does not allow streaming, or the API key lacks the stream scope.No, not with the same credentials
4409reconnectRole, active org, plan or API key scopes changed; fetch a new ticket if you use one, reconnect and resume with since.Yes, with backoff; resume with since
4429too_many_connectionsThe org's stream connection quota is used up (API keys), or the user or session already holds its maximum of open connections (signed-in clients; 20 per user and 10 per session by default); close another connection or retry later.Yes, with backoff; resume with since

Slow consumers (4008) should read faster or subscribe to less; idle timeouts (1001) mean your client didn’t answer pings, which WebSocket libraries do by default, or send ping ops.

  • Connections. Each open connection of an API key holds one of your organization’s connection slots: 1 on Pro, 5 on Elite, 20 on Enterprise. Use one connection with many subscriptions rather than one connection per topic. One more closes with 4429.
  • Signed-in connections (the app, and tickets issued to a session rather than a key) are capped per user (20) and per session (10) across all servers; one more closes with 4429 too_many_connections, which is retryable.
  • Subscriptions. At most your plan’s subscriptions per connection: 50 on Pro, 500 on Elite, 2,000 on Enterprise. One more answers a subscription_limit error.
  • Your messages. At most 20 per second, in bursts of 50; beyond that the server answers rate_limited errors.
  • Flow control. The server buffers 512 messages per connection. If you can’t keep up, the connection closes with 4008 rather than delaying everyone else; reconnect and resume.
  • Entitlements apply per message: you only receive the markets your plan includes. See entitlements.

Stream messages don’t count against REST quotas.