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.
Connecting
Section titled “Connecting”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.
#!/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 pipefailAPI="${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.
Subscribing
Section titled “Subscribing”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 aheartbeatechoing the id. The server also sends aheartbeatevery 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 answersunknown_topicwithunknown event <id>.
// 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());# Your organization's alert firings, live. The client reconnects on its own,# resumes after the last seq it delivered, and drops duplicates.import asyncioimport os
from status302 import AsyncStatus302, DataMessage, ErrorMessage, ResyncMessage
async def main() -> None: api = os.environ.get("STATUS302_API_URL", "https://api.status302.com") async with AsyncStatus302(os.environ["STATUS302_API_KEY"], base_url=api) as status302: async with status302.stream() as stream: alerts = stream.subscribe("alerts") print(f"subscribed to alerts at seq {await alerts.ready}", flush=True) async for message in stream: if isinstance(message, DataMessage): print(f"alert #{message.seq} at {message.ts}: {message.data['title']}", flush=True) elif isinstance(message, ResyncMessage): # The gap could not be replayed: reload the REST snapshot, then continue live. snapshot = await stream.fetch_snapshot(message) print(f"resynced {message.topic} ({message.reason}) from {message.snapshot}", snapshot) elif isinstance(message, ErrorMessage): print(f"subscription refused: {message.code}: {message.message}")
asyncio.run(main())Topics
Section titled “Topics”| 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.
Filters
Section titled “Filters”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.
Resuming after a reconnect
Section titled “Resuming after a reconnect”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.
Resync
Section titled “Resync”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.
Close codes
Section titled “Close codes”Connections end with a WebSocket close code. Reconnect with backoff for the retryable ones; the others need new credentials or a plan change first.
| Code | Name | Meaning | Reconnect? |
|---|---|---|---|
1001 | going_away | Idle 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 |
1009 | message_too_big | A client message exceeded 64 KiB. | Yes, with backoff; resume with since |
1012 | service_restart | The server is restarting; reconnect and resume with since. | Yes, with backoff; resume with since |
4008 | slow_consumer | The connection's send buffer overflowed; reconnect and resume with since. | Yes, with backoff; resume with since |
4401 | unauthorized | No 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 |
4403 | forbidden | The plan does not allow streaming, or the API key lacks the stream scope. | No, not with the same credentials |
4409 | reconnect | Role, 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 |
4429 | too_many_connections | The 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.
Limits
Section titled “Limits”- 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
4429too_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_limiterror. - Your messages. At most 20 per second, in bursts of 50; beyond that the server answers
rate_limitederrors. - Flow control. The server buffers 512 messages per connection. If you can’t keep up, the connection closes with
4008rather 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.

