> ## Documentation Index
> Fetch the complete documentation index at: https://docs.therundown.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Multiplexed WebSocket

> Subscribe to markets, scores, play-by-play, and live game state over a single connection to /api/v2/ws.

The multiplexed WebSocket at `wss://therundown.io/api/v2/ws` carries multiple logical channels — `markets`, `scores`, `plays`, and `live` — over one connection. Instead of encoding filters in the connection URL, you send JSON subscribe messages after connecting, and you can add or remove subscriptions at any time without reconnecting.

<Note>
  WebSocket access is enabled on real-time API tiers (Ultra and above) by default. The `plays` and `live` channels additionally require the live game state entitlement, which is also included from Ultra up — see [Rate Limits](/rate-limits#current-api-tier-defaults). Each plan also caps concurrent connections and subscriptions per connection — see the [connection limits table](/api-reference/v2/websocket#overview).
</Note>

## When to Use the Multiplexed Endpoint

| If you need...                                         | Use                                                                                                           |
| ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------- |
| Market price updates only, with a fixed set of filters | [`/api/v2/ws/markets`](/api-reference/v2/websocket#markets-websocket) — simpler, filters via query parameters |
| Scores, play-by-play, or live game state streams       | `/api/v2/ws` — these channels are only available here                                                         |
| Multiple feeds with independent filters                | `/api/v2/ws` — one subscription per feed, one connection                                                      |
| To change filters at runtime                           | `/api/v2/ws` — unsubscribe and resubscribe without reconnecting                                               |

## Connect

Authenticate with the `key` query parameter, the same as the dedicated markets stream:

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

The connection starts with no subscriptions. Until you subscribe, the only messages you receive are [heartbeats](/api-reference/v2/websocket#heartbeat) every 15 seconds.

## Subscribe to a Channel

Send a `subscribe` action with a client-chosen `id`, the `channel`, and optional `params` filters:

```json theme={null}
{
  "action": "subscribe",
  "id": "nba-markets",
  "channel": "markets",
  "params": { "sport_ids": [4], "market_ids": [1, 2, 3] }
}
```

| Field     | Required              | Description                                                                                                             |
| --------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `action`  | Yes                   | `"subscribe"`, `"unsubscribe"`, or `"snapshot"` (see [Request a Snapshot](#request-a-snapshot))                         |
| `id`      | Yes                   | Client-chosen identifier, unique among your active subscriptions. Every message for this subscription is tagged with it |
| `channel` | `subscribe` only      | `"markets"`, `"scores"`, `"plays"`, or `"live"`                                                                         |
| `params`  | No (`subscribe` only) | Filters for this subscription. Without filters, you receive everything the channel carries                              |

The server acknowledges each successful subscription:

```json theme={null}
{ "type": "subscribed", "id": "nba-markets", "sequence": 12, "message": "subscribed to markets" }
```

### Channels and Filters

| Channel   | Delivers                                                                                                                                                                                              | `params` filters                                        |
| --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
| `markets` | Market price updates — same payloads as [`/api/v2/ws/markets`](/api-reference/v2/websocket#markets-websocket)                                                                                         | `sport_ids`, `event_ids`, `market_ids`, `affiliate_ids` |
| `scores`  | Score and event-status updates (no live game state fields)                                                                                                                                            | `sport_ids`, `event_ids`                                |
| `plays`   | Play-by-play deltas for live games (**Ultra plan or higher**)                                                                                                                                         | `sport_ids`, `event_ids`                                |
| `live`    | Everything in-game on one subscription: score/status deltas including `live_game_state`, plus play-by-play deltas (**Ultra plan or higher**). `live_game_state` and `game_state` are accepted aliases | `sport_ids`, `event_ids`                                |

<Warning>
  Unlike REST query parameters, `params` values are JSON arrays, not comma-separated strings: numbers for `sport_ids`, `market_ids`, and `affiliate_ids` (e.g., `[4, 6]`), strings for `event_ids`.
</Warning>

## Handle Incoming Messages

Channel data arrives wrapped in a delta envelope tagged with your subscription `id`. Route messages on the top-level `type` field, then on `id`:

```json theme={null}
{
  "type": "delta",
  "id": "nba-markets",
  "sequence": 42,
  "sub_sequence": 7,
  "delta_last_id": "...",
  "data": {
    "meta": { "type": "market_price", "version": "v2", "timestamp": 1772495104 },
    "data": {
      "event_id": "9b9d0cf6007fdaeb15c3a1888dcfd5df",
      "affiliate_id": 26,
      "market_id": 3,
      "line": "1.5",
      "price": "-117",
      "previous_price": "-122.0000",
      "price_delta": 5,
      "is_main_line": true,
      "sport_id": 7,
      "updated_at": "2026-03-02T23:44:44Z"
    }
  }
}
```

| Field           | Description                                                                                                                 |
| --------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `type`          | `"delta"` for channel data. Other top-level types are `"subscribed"`, `"snapshot"`, `"snapshot_complete"`, and `"error"`    |
| `id`            | The subscription `id` you chose when subscribing — use it to route the message                                              |
| `sequence`      | Server-assigned sequence number for messages on this connection                                                             |
| `sub_sequence`  | Message counter within this subscription                                                                                    |
| `delta_last_id` | Cursor of the underlying delta feed for this change                                                                         |
| `data`          | The channel payload — an object with its own `meta` and `data`, in the same format as the dedicated stream for that channel |

Heartbeats arrive in the same format as on the dedicated markets stream — `{"meta": {"type": "heartbeat"}, ...}` — and are not tagged with a subscription `id`. For the full payload field reference per channel, see the [WebSocket reference](/api-reference/v2/websocket#message-format); play-by-play payloads are documented under [Play messages](/api-reference/v2/websocket#play-messages).

## Request a Snapshot

Deltas only tell you what changed — to start from current state, add `"snapshot": true` to `params` when subscribing. Snapshot requests need a bounded scope: `event_ids`, or `sport_ids` plus `date` (`plays` snapshots require `event_ids`):

```json theme={null}
{
  "action": "subscribe",
  "id": "nba-markets",
  "channel": "markets",
  "params": { "sport_ids": [4], "date": "2026-07-19", "market_ids": [1, 2, 3], "snapshot": true }
}
```

After the `subscribed` ack, the server sends one or more `snapshot` frames followed by `snapshot_complete`; live deltas begin after that. An active subscription can also request a fresh snapshot at any time without reconnecting:

```json theme={null}
{ "action": "snapshot", "id": "nba-markets" }
```

Snapshot frames are metered as data points by category, the same as the equivalent REST reads. See [Snapshots](/api-reference/v2/websocket#snapshots) in the reference for per-channel snapshot contents.

## Unsubscribe

Send an `unsubscribe` action with the subscription's `id`:

```json theme={null}
{ "action": "unsubscribe", "id": "nba-markets" }
```

To change a subscription's filters, unsubscribe and subscribe again. If your application cannot tolerate a gap between the two, subscribe with the new filters under a **different** `id` first, then unsubscribe the old one — you may briefly receive duplicate messages while both are active, so deduplicate during the overlap.

## Handle Errors

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

| Code                 | Meaning                                                                                         | What to do                                                                            |
| -------------------- | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| `forbidden`          | Subscribing to `plays` or `live` without the live game state entitlement (Ultra plan or higher) | Upgrade your plan, or drop the gated subscription                                     |
| `invalid_channel`    | Unknown `channel` value                                                                         | Use `markets`, `scores`, `plays`, or `live`                                           |
| `missing_id`         | Subscribe action sent without an `id`                                                           | Include a client-chosen `id`                                                          |
| `duplicate_id`       | An active subscription already uses this `id`                                                   | Pick a different `id`, or unsubscribe the existing one first                          |
| `subscription_limit` | Your plan's concurrent subscription cap was reached                                             | Unsubscribe an existing subscription, or consolidate filters into fewer subscriptions |

## Complete Client Example

A client that subscribes to NBA market prices and scores on one connection, routes messages by subscription `id`, and resubscribes automatically after a reconnect:

<CodeGroup>
  ```javascript JavaScript theme={null}
  const API_KEY = "YOUR_API_KEY";
  const WS_URL = `wss://therundown.io/api/v2/ws?key=${API_KEY}`;

  const SUBSCRIPTIONS = [
    {
      action: "subscribe",
      id: "nba-markets",
      channel: "markets",
      params: { sport_ids: [4], market_ids: [1, 2, 3] },
    },
    {
      action: "subscribe",
      id: "nba-scores",
      channel: "scores",
      params: { sport_ids: [4] },
    },
  ];

  function handleDelta(msg) {
    const payload = msg.data; // { meta: {...}, data: {...} }

    switch (msg.id) {
      case "nba-markets": {
        const d = payload.data;
        console.log(
          `Price: event=${d.event_id} market=${d.market_id} ${d.price} (was ${d.previous_price})`
        );
        break;
      }
      case "nba-scores":
        console.log("Score update:", payload.data);
        break;
    }
  }

  function connect() {
    const ws = new WebSocket(WS_URL);

    ws.onopen = () => {
      console.log("Connected — subscribing");
      // Subscriptions only live as long as the connection,
      // so re-send them on every (re)connect
      SUBSCRIPTIONS.forEach((sub) => ws.send(JSON.stringify(sub)));
    };

    ws.onmessage = (event) => {
      const msg = JSON.parse(event.data);

      if (msg.meta?.type === "heartbeat") return;

      switch (msg.type) {
        case "subscribed":
          console.log(`Subscription ${msg.id} active: ${msg.message}`);
          break;
        case "delta":
          handleDelta(msg);
          break;
        case "error":
          console.error(`Subscription error [${msg.code}]: ${msg.message}`);
          break;
      }
    };

    ws.onclose = () => {
      console.log("Disconnected, reconnecting in 3s...");
      setTimeout(connect, 3000);
    };

    ws.onerror = (error) => {
      console.error("WebSocket error:", error);
      ws.close();
    };
  }

  connect();
  ```

  ```python Python theme={null}
  import asyncio
  import json
  import websockets

  API_KEY = "YOUR_API_KEY"
  WS_URL = f"wss://therundown.io/api/v2/ws?key={API_KEY}"

  SUBSCRIPTIONS = [
      {
          "action": "subscribe",
          "id": "nba-markets",
          "channel": "markets",
          "params": {"sport_ids": [4], "market_ids": [1, 2, 3]},
      },
      {
          "action": "subscribe",
          "id": "nba-scores",
          "channel": "scores",
          "params": {"sport_ids": [4]},
      },
  ]


  def handle_delta(msg):
      payload = msg["data"]  # {"meta": {...}, "data": {...}}

      if msg["id"] == "nba-markets":
          d = payload["data"]
          print(
              f"Price: event={d['event_id']} market={d['market_id']}"
              f" {d['price']} (was {d['previous_price']})"
          )
      elif msg["id"] == "nba-scores":
          print(f"Score update: {payload['data']}")


  async def listen():
      while True:
          try:
              async with websockets.connect(WS_URL) as ws:
                  print("Connected — subscribing")
                  # Subscriptions only live as long as the connection,
                  # so re-send them on every (re)connect
                  for sub in SUBSCRIPTIONS:
                      await ws.send(json.dumps(sub))

                  async for raw in ws:
                      msg = json.loads(raw)

                      if msg.get("meta", {}).get("type") == "heartbeat":
                          continue

                      msg_type = msg.get("type")
                      if msg_type == "subscribed":
                          print(f"Subscription {msg['id']} active: {msg['message']}")
                      elif msg_type == "delta":
                          handle_delta(msg)
                      elif msg_type == "error":
                          print(f"Subscription error [{msg['code']}]: {msg['message']}")

          except (websockets.ConnectionClosed, ConnectionError):
              print("Disconnected, reconnecting in 3s...")
              await asyncio.sleep(3)


  asyncio.run(listen())
  ```
</CodeGroup>

For production use, replace the fixed 3-second reconnect delay with exponential backoff and jitter — see [Reconnection Best Practices](/guides/websocket-streaming#reconnection-best-practices).

## Streaming Play-by-Play

On an Ultra plan or higher, add a `plays` subscription to receive live play-by-play for in-progress games:

```json theme={null}
{
  "action": "subscribe",
  "id": "mlb-plays",
  "channel": "plays",
  "params": { "sport_ids": [3], "event_ids": ["816efd1e5767d7133b5bc70c77173a18"] }
}
```

Each play arrives in the delta envelope with `meta.type` of `"play"` in the inner payload — description, period, running score, and (as attribution rolls out) the players involved. For the full play payload, see [Play messages](/api-reference/v2/websocket#play-messages) in the reference.

To load the plays that happened before you subscribed, either add `"snapshot": true` to the subscription (`plays` snapshots return up to 500 current plays), or fetch the timeline from the REST endpoint [`GET /api/v2/events/{eventID}/plays`](/api-reference/generated/v2-events/get-play-by-play-for-an-event) and apply streamed plays on top.

If you want scores, `live_game_state`, and plays together for the same games, subscribe to the `live` channel instead of pairing `scores` with `plays` — it delivers all in-game updates on one subscription (score/status deltas arrive with `meta.type` of `"score"`, plays with `"play"`), which also conserves your plan's subscription slots.

## Best Practices

<AccordionGroup>
  <Accordion title="Resubscribe on every reconnect">
    Subscriptions live only as long as the connection. After any disconnect, re-send all subscribe messages as soon as the new connection opens — put the subscribe logic in your `onopen` handler so it runs on every connect, as in the example above.
  </Accordion>

  <Accordion title="Re-sync state after a reconnect">
    You may have missed updates while disconnected. Resubscribe with `"snapshot": true` so each subscription replays current state before deltas resume, or fetch current state from the REST API (or the [delta endpoints](/guides/efficient-polling)). Either way your local cache won't serve stale prices or scores; snapshot frames are metered like the equivalent REST reads.
  </Accordion>

  <Accordion title="Filter every subscription">
    An unfiltered `markets` subscription streams every price change across all sports and books, and the connection's [1024-message buffer](/api-reference/v2/websocket#message-queue) drops messages if you fall behind. Scope each subscription with `sport_ids`, `event_ids`, or `market_ids` to what you actually consume.
  </Accordion>

  <Accordion title="Use meaningful subscription ids">
    The `id` is how you route incoming messages, so name subscriptions after what they carry (`nba-markets`, `mlb-plays`) rather than opaque strings. Keep a map of active ids to handlers and you can add or remove feeds without touching your message loop.
  </Accordion>

  <Accordion title="Consolidate subscriptions where filters allow">
    Plans cap subscriptions per connection — from 3 on Ultra to 50 on Enterprise (error code `subscription_limit`; see the [connection limits table](/api-reference/v2/websocket#overview)). One `markets` subscription filtered to two sports uses one slot; two single-sport subscriptions use two. The `live` channel replaces a `scores` + `plays` pair with a single slot. Split subscriptions only when you need to route or manage the feeds independently.
  </Accordion>
</AccordionGroup>

## Next Steps

<CardGroup cols={2}>
  <Card title="WebSocket Reference" icon="book" href="/api-reference/v2/websocket">
    Full protocol details and payload field tables
  </Card>

  <Card title="WebSocket Streaming" icon="bolt" href="/guides/websocket-streaming">
    The dedicated markets stream, heartbeats, and reconnection patterns
  </Card>

  <Card title="Play-by-Play REST Endpoint" icon="list" href="/api-reference/generated/v2-events/get-play-by-play-for-an-event">
    Backfill the play timeline before streaming
  </Card>

  <Card title="Efficient Polling" icon="rotate" href="/guides/efficient-polling">
    REST delta endpoints as a fallback or backfill
  </Card>
</CardGroup>
