Skip to main content

Overview

TheRundown provides a V2 WebSocket endpoint for streaming real-time data without polling: WebSocket connections do not count against your REST API rate limit — but each plan has a hard cap on concurrent connections (and on subscriptions per connection on the multiplexed endpoint). Opening a connection beyond the cap is rejected at upgrade time with a 429 (WebSocket connection limit reached).
WebSocket access requires a real-time API tier (Ultra and above). The plays, stats, and live channels additionally require the live game state entitlement, which is also included from Ultra up — see Rate Limits.

Connection

Connect using a standard WebSocket client. Authentication is via the key query parameter:
Use wss:// (secure WebSocket), not ws://. Connections on ws:// will be rejected.

Markets WebSocket

GET /api/v2/ws/markets

Streams V2 market price changes in real time. Every time a sportsbook updates a price on any tracked market, you receive a message.

Filter Parameters

All filters are optional. If none are specified, you receive all messages across all sports and markets.

Example Connection URLs

Live frames carry the in-play market IDs. A market_ids=1,2,3 filter automatically includes the live variants 41,42,43 server-side, so live full-game updates DO stream — but each frame’s data.market_id is the in-play ID (41/42/43), never relabeled to 1/2/3. Clients that match strictly on the prematch IDs silently drop the live stream. Accept both ID families (or subscribe to the live IDs explicitly); half markets keep the same IDs in-play (4,5,6 first half, 1008,1009,1010 second half). See Market IDs — Live / In-Play. Every delivered message is metered as data points, so a tight market_ids + affiliate_ids + event_ids scope is also how you control usage.

Message Format

Each message is a JSON object with two top-level fields: meta (message metadata) and data (the update payload). Unlike the REST API’s nested structure, WebSocket messages deliver one price update per message in a flat format — each message represents a single price change for one participant, one market, and one sportsbook.

Example: Market Price Update

The example above is a Polymarket price (affiliate_id: 26), so it carries liquidity_usd. A price from a traditional sportsbook affiliate omits the field entirely instead of sending null or 0.

Meta Fields

Data Fields

liquidity_usd rides along on whatever price update was already being sent — a liquidity-only change never produces its own message, so you won’t see a delta for it in isolation. It appears the same way on snapshot frames ("snapshot": true) as it does on live deltas, since snapshots reflect the same current-price data as the REST endpoints. See What is liquidity_usd on Kalshi and Polymarket prices? for how each exchange computes it and how fresh it is.

Multiplexed WebSocket Channels

The multiplexed endpoint at /api/v2/ws carries multiple logical channels over a single connection. Instead of query-parameter filters, you send JSON subscribe messages after connecting:
For a step-by-step client walkthrough of this endpoint — connecting, subscribing, routing messages, and reconnecting — see the Multiplexed WebSocket guide.
The plays, stats, and live channels require the live game state entitlement (Ultra plan or higher) — the same entitlement as the REST plays endpoint. The futures channel requires the futures entitlement (Ultra plan or higher for API keys; Pro or higher for web sessions), matching the REST futures endpoints. The markets and scores channels are available to any WebSocket-entitled key.

Subscribing

Send a subscribe action with a client-chosen id, the channel, and optional filters:
The server acknowledges with:
Filters supported in params: sport_ids, event_ids (plus market_ids, affiliate_ids, and main_line on the markets and futures channels). The stats channel does not support stats_ids, team, or player filters; market_ids and affiliate_ids do not apply to it. "main_line": true filters the live stream (and, combined with "snapshot": true, the snapshot) to main-line rows only: every price move of the current main and every close of the main. A re-designation onto a line whose price also moved arrives immediately; a designation-only change (price unchanged) surfaces on that line’s next price update — pair with a periodic REST refresh (main_line=true) for the authoritative current main. Alternate-line updates are not delivered and not metered — for a per-book best-line tracker this typically cuts delivered volume by 60–75% on spread/total markets.

Snapshots

Add "snapshot": true to params to receive the current state before live deltas begin. The server sends the subscribed ack, one or more snapshot frames, then snapshot_complete; deltas start after that. Each snapshot covers your subscription scope only — markets snapshots use the same filters and market defaults as the REST events endpoints (each frame carries scope.requested_market_ids / scope.returned_market_ids), live snapshots include the current score/status and live game state, and plays snapshots return up to 500 current plays with a cursor holding the oldest/newest play sequence. futures snapshots replay the REST futures endpoints — one frame per sport with the full board including settlement state. Snapshot requests need a bounded scope: event_ids, or sport_ids plus date (play snapshots require event_ids; futures snapshots require positive sport_ids with no date, and event_ids scoping there requires exactly one sport). An existing subscription can request a fresh snapshot at any time without reconnecting:
Snapshot frames are metered as data points by category, the same as the equivalent REST reads.
The stats channel is delta-only and does not support snapshots, resync, resume, or replay. A snapshot request returns snapshot_error with code unsupported_snapshot, and a live snapshot does not contain a game-stat baseline. On first connect, reconnect, or a detected gap, fetch both GET /api/v2/events/{eventID}/stats and GET /api/v2/events/{eventID}/players/stats, then apply new deltas.

Play messages

Each play arrives wrapped in a delta envelope tagged with your subscription id:
To stop receiving a channel, send {"action": "unsubscribe", "id": "p1"}.

Game stats messages

Subscribe on the multiplexed endpoint, not the dedicated /api/v2/ws/markets feed, and scope the subscription by sport, event, or both:
For supported live games, team and player box-score changes stream at play latency — typically within a few seconds of the corresponding play-by-play update (live game data as a whole trails the on-field action by roughly 15–20 seconds, in line with the typical broadcast delay). Each frame contains only stat rows whose value changed; it is not a complete box score:
Each changed row across team_stats[].stats and player_stats[].stats consumes one data point in the stats category, including when the frame arrives through the live channel. A valid game_stats frame with no nested rows — either a completion marker or an invalidation fallback — consumes one stats data point. If one inbound frame matches overlapping stats and live subscriptions on the same connection, it is delivered to both subscriptions but billed once on that connection.
A game_stats frame with both stat arrays absent has one of two meanings:
  • If complete is true, it is the terminal completion marker. It includes event_id, sport, sport_id, and an RFC 3339 updated_at. Mark the cached box score complete; repeated markers are idempotent. Refetch only if you do not have a baseline or your normal gap policy requires it.
  • If complete is absent, it is the legacy invalidation fallback. It contains event_id, sport_id, and a numeric Unix-seconds updated_at. Refetch both REST box-score resources.
Parse updated_at tolerantly across row deltas, completion markers, and invalidation fallbacks.
The outer sub_sequence can help detect a gap while the subscription is live, but neither it nor delta_last_id can replay game-stat changes. delta_last_id is the markets-delta cursor even when it appears on a stats frame. Recovery is always a REST refetch followed by continued streaming.

Error codes

Errors are returned as {"type": "error", "id": "...", "code": "...", "message": "..."}:

Heartbeat

The WebSocket endpoint sends a heartbeat message every 15 seconds to keep the connection alive:
Your client should detect heartbeats and use them to confirm the connection is healthy. If you stop receiving heartbeats, the connection may have dropped — reconnect.

Message Queue

On the multiplexed endpoint, every non-market subscription has its own 1024-message outbound queue. The markets channel is sized separately for its higher message volume. If a subscription or fan-out queue cannot accept a live frame, the server closes the connection rather than let it continue with a silent gap. When possible, it first sends:
The following close frame has reason buffer_overflow:reconnect_and_catchup. Reconnect, re-send every subscription, and restore current state before applying new deltas. For stats, fetch both event game-stat REST resources and merge newly buffered deltas because snapshots and replay are unsupported. Keep handlers fast and offload heavy work asynchronously. Narrow each subscription with the supported sport, event, market, and affiliate filters to reduce queue pressure.

Client Examples


If WebSocket is not an option for your architecture, use the REST delta endpoints to poll for changes efficiently. For the full list of market types you can filter on, see Market IDs.

Best Practices

The unfiltered market feed can be very high volume. Always apply sport_ids, market_ids, or event_ids filters to receive only the data you need. On the multiplexed endpoint, queue overflow closes the connection and requires a reconnect plus state recovery.
WebSocket connections can drop due to network issues, server deployments, idle timeouts, or multiplexed queue overflow. Always implement reconnection logic with exponential backoff (e.g., 1s, 2s, 4s, 8s, max 30s). After buffer_overflow:reconnect_and_catchup, restore current state from REST before applying new deltas.
If you have not received any message (including heartbeats) for 30+ seconds, assume the connection is dead and reconnect. Do not wait for the WebSocket close event, as it may not fire reliably in all network conditions.
Keep your message handler fast. Parse the JSON and push work to a queue or separate processing thread. On the multiplexed endpoint, a slow handler can fill a subscription’s outbound queue, causing the server to close the connection rather than continue after a silent gap.