---
title: "Multiplexed WebSocket — subscribe to markets, scores, plays, stats, and live channels"
method: GET
path: "/api/v2/ws"
tags: ["V2 WebSocket"]
---

# Multiplexed WebSocket — subscribe to markets, scores, plays, stats, and live channels

`GET /api/v2/ws`

Establishes a single WebSocket connection carrying multiple logical channels.
Instead of query-parameter filters, send JSON subscribe messages after connecting.

**Connection**: `wss://therundown.io/api/v2/ws?key=YOUR_API_KEY`

**Channels**:
- `markets` — market price updates (same payloads as `/api/v2/ws/markets`)
- `scores` — score and event-status updates (no live game state fields)
- `plays` — play-by-play deltas for live games. **Requires an Ultra plan or higher**
  (the live game state entitlement); a `plays`, `stats`, or `live` subscribe from a
  non-entitled key is rejected with error code `forbidden`.
- `stats` — changed team and player box-score rows, delivered inline with
  `meta.type=game_stats`. **Requires an Ultra plan or higher**. This is a
  delta-only channel with no snapshot, replay, or resume; bootstrap and recover
  from both event game-stat REST endpoints. Each changed stat row is one stats
  data point.
- `live` — score/status deltas including `live_game_state`, play-by-play deltas,
  and game-stat deltas, on one subscription. **Requires an Ultra plan or higher**;
  `live_game_state` and `game_state` are accepted aliases.

**Subscribe** (client → server):
```json
{
  "action": "subscribe",
  "id": "p1",
  "channel": "plays",
  "params": { "sport_ids": [3], "event_ids": ["<eventID>"] }
}
```
The server acknowledges with `{"type":"subscribed","id":"p1","sequence":N,"message":"subscribed to plays"}`.
Supported `params` filters: `sport_ids`, `event_ids` (plus `market_ids` and
`affiliate_ids` on the `markets` channel). `stats` has no stat, team, or player
filter. Unsubscribe with
`{"action":"unsubscribe","id":"p1"}`.

**Delta messages** (server → client) are wrapped in an envelope tagged with your
subscription `id`:
```json
{
  "type": "delta",
  "id": "p1",
  "sequence": 42,
  "sub_sequence": 7,
  "delta_last_id": "...",
  "data": { "meta": { "type": "play" }, "data": { ... } }
}
```

A stats delta uses the same outer envelope. Its inner payload has
`meta.type=game_stats`; `data.team_stats[]` and `data.player_stats[]` contain
only changed rows, and each row's `value` remains a JSON string. Upsert those
rows into the REST-bootstrapped box score rather than replacing a whole group.
If both row arrays are absent and `complete=true`, the frame is the terminal
completion marker; mark the cached box complete and treat repeats as idempotent.
If both arrays are absent without `complete=true`, the frame is an invalidation
fallback: refetch both `GET /api/v2/events/{eventID}/stats` and
`GET /api/v2/events/{eventID}/players/stats`. Row deltas and completion markers
use an RFC 3339 `updated_at`; the fallback uses numeric Unix seconds. Do not use
`delta_last_id` as a game-stat replay cursor. Each changed nested row is one
stats data point; either zero-row variant costs one stats data point.

**Snapshots**: add `"snapshot": true` to `params` to receive current state
(`snapshot` frames, then `snapshot_complete`) before deltas begin; an active
subscription can request a fresh snapshot at any time with
`{"action":"snapshot","id":"p1"}`. Snapshot requests need a bounded scope and
are metered as data points like the equivalent REST reads. Snapshots are not
supported on `stats`; a request returns `snapshot_error` with code
`unsupported_snapshot`. A `live` snapshot does not include a game-stat baseline.

**Errors**: `{"type":"error","id":"...","code":"...","message":"..."}` with codes
`forbidden` (plays/stats/live without Ultra+), `invalid_channel`, `missing_id`,
`duplicate_id`, and `subscription_limit` (plan's concurrent subscription cap
reached). A slow multiplexed client may receive the connection-level error code
`buffer_overflow` immediately before the connection closes with reason
`buffer_overflow:reconnect_and_catchup`.

**Queue and recovery**: each non-market subscription has its own 1024-message
outbound queue; market subscriptions are sized separately. If a live frame
cannot be queued, the server closes the connection rather than continue with a
silent gap. Reconnect, re-send subscriptions, and recover current state before
applying new deltas. For `stats`, refetch both event game-stat REST resources.

Concurrent connection and subscription limits vary by tier. See the
[WebSocket reference](/api-reference/v2/websocket) for full protocol details.

## Other responses

- `101` — WebSocket upgrade successful

---

[API](https://skmtc.net/therundown/apis/therundown-sports-api.md) · [All operations](https://skmtc.net/therundown/apis/therundown-sports-api/llms.txt) · [OpenAPI document](https://skmtc-service-staging.skmtc.workers.dev/v1/apis/therundown/therundown-sports-api/versions/f069d4e82f52/schema)
