> ## 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.

# WebSocket Streaming

> Connect to TheRundown WebSocket feeds for real-time odds, scores, plays, and game-stat updates.

TheRundown provides WebSocket endpoints for real-time data delivery. Instead of polling REST endpoints, open a persistent connection and receive updates as they happen. WebSocket traffic does not increment the HTTP request counter, but pushed messages and snapshots are still metered as data points.

<Note>
  WebSocket access requires a real-time API tier (Ultra and above). If your key does not have WebSocket access, use the [market delta endpoint](/guides/efficient-polling) for ongoing updates.
</Note>

## Available WebSocket Endpoints

| Endpoint | URL | Description |
| - | - | - |
| V2 Markets | `wss://therundown.io/api/v2/ws/markets` | Real-time market/price updates for all V2 data |
| V2 Multiplexed | `wss://therundown.io/api/v2/ws` | Subscribe to `markets`, `scores`, `plays`, `stats`, and `live` channels over one connection. The `plays`, `stats`, and `live` channels require an **Ultra plan or higher** |
| V1 (Legacy) | `wss://therundown.io/api/v1/ws` | Legacy format line updates |

This guide covers the V2 Markets stream. For the multiplexed endpoint — connecting, subscribing to channels, and streaming play-by-play or live game stats — see the [Multiplexed WebSocket guide](/guides/multiplexed-websocket) and the [WebSocket reference](/api-reference/v2/websocket#multiplexed-websocket-channels).

## V2 Markets WebSocket

The primary WebSocket for real-time odds data. Delivers individual price updates in a flat format — each message contains one price change for one participant, market, and sportsbook.

### Connection

Connect to `wss://therundown.io/api/v2/ws/markets` from a server-side client and
send `X-TheRundown-Key: $THERUNDOWN_API_KEY` in the upgrade request. Native
browser WebSocket clients cannot set custom headers; have the browser connect to
your authenticated backend relay instead.

### Filter Parameters

All filters are optional. Without filters, you receive updates for all sports and markets.

| Parameter | Type | Description |
| - | - | - |
| `sport_ids` | string | Comma-separated sport IDs (e.g., `2,4,6`) |
| `market_ids` | string | Comma-separated market IDs (e.g., `1,2,3`) |
| `event_ids` | string | Comma-separated event IDs for specific games |
| `affiliate_ids` | string | Comma-separated sportsbook IDs |

### Example: Filtered Connection

```
wss://therundown.io/api/v2/ws/markets?sport_ids=4&market_ids=1,2,3&affiliate_ids=19,23
```

### Message Format

Each message is a JSON object with `meta` (message metadata) and `data` (the price update). Each message represents a single price change — not a full market snapshot.

```json theme={null}
{
  "meta": {
    "type": "market_price",
    "version": "v2",
    "timestamp": 1772495104
  },
  "data": {
    "id": 193600383,
    "event_id": "9b9d0cf6007fdaeb15c3a1888dcfd5df",
    "affiliate_id": 26,
    "market_participant_id": 19402291,
    "market_id": 3,
    "line": "1.5",
    "price": "-117",
    "previous_price": "-122.0000",
    "price_delta": 5,
    "is_main_line": true,
    "normalized_market_participant_id": 10,
    "normalized_market_participant_type": 3,
    "sport_id": 7,
    "updated_at": "2026-03-02T23:44:44Z"
  }
}
```

### Code Examples

For Node.js, install `ws` with `npm install ws`. The Python examples use
`websockets>=14`, where `additional_headers` adds request headers to the
handshake.

<CodeGroup>
  ```javascript Node.js 22+ (ws) theme={null}
  const WebSocket = require("ws");
  const apiKey = process.env.THERUNDOWN_API_KEY;
  if (!apiKey) throw Error('Set THERUNDOWN_API_KEY');

  const ws = new WebSocket(
    "wss://therundown.io/api/v2/ws/markets?sport_ids=4&market_ids=1,2,3",
    { headers: { "X-TheRundown-Key": apiKey } }
  );

  ws.on("open", () => {
    console.log("Connected to V2 Markets WebSocket");
  });

  ws.on("message", (raw) => {
    const msg = JSON.parse(raw.toString());

    // Handle heartbeat
    if (msg.meta?.type === "heartbeat") {
      console.log("Heartbeat received");
      return;
    }

    // Process market price update
    const d = msg.data;
    console.log(
      `Update: event=${d.event_id} market=${d.market_id} aff=${d.affiliate_id}`
    );
    console.log(
      `  line=${d.line} price=${d.price} (was ${d.previous_price}, delta=${d.price_delta})`
    );
  });

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

  ws.on("close", (code, reason) => {
    console.log(`WebSocket closed: code=${code} reason=${reason}`);
  });
  ```

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

  API_KEY = os.environ["THERUNDOWN_API_KEY"]
  WS_URL = (
      f"wss://therundown.io/api/v2/ws/markets"
      f"?sport_ids=4&market_ids=1,2,3"
  )


  async def listen():
      async with websockets.connect(
          WS_URL, additional_headers={"X-TheRundown-Key": API_KEY}
      ) as ws:
          print("Connected to V2 Markets WebSocket")

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

              # Handle heartbeat
              if msg.get("meta", {}).get("type") == "heartbeat":
                  print("Heartbeat received")
                  continue

              # Process market price update
              d = msg["data"]
              print(
                  f"Update: event={d['event_id']} market={d['market_id']} aff={d['affiliate_id']}"
              )
              print(
                  f"  line={d['line']} price={d['price']}"
                  f" (was {d['previous_price']}, delta={d['price_delta']})"
              )


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

## V1 WebSocket (Legacy)

The V1 WebSocket delivers line updates with `meta.type` identifying the market type (e.g., `"moneyline"`, `"spread"`, `"total"`). The `data` object contains market-specific fields with `_delta` suffixes showing the change from the previous value. Use this only if your application has not migrated to V2.

### Connection

Use V1 only for an existing legacy integration. New streaming integrations
should use V2, which authenticates the WebSocket upgrade with the request
header above.

### Filter Parameters

| Parameter | Type | Description |
| - | - | - |
| `sport_ids` | string | Comma-separated sport IDs |
| `affiliate_ids` | string | Comma-separated sportsbook IDs |
| `event_ids` | string | Comma-separated event IDs |
| `date` | string | Date filter (e.g., `2026-02-12`) |

### Example Message

```json theme={null}
{
  "meta": {
    "type": "spread"
  },
  "data": {
    "point_spread_away": 0.5,
    "point_spread_home": -0.5,
    "point_spread_away_delta": 0.4999,
    "point_spread_home_delta": -0.5001,
    "point_spread_away_money": 115,
    "point_spread_away_money_delta": 114.9999,
    "point_spread_home_money": -175,
    "point_spread_home_money_delta": -175.0001,
    "line_id": 17994923,
    "event_id": "541a8c85c41cd7ec7ecc74521ee71bc4",
    "sport_id": 12,
    "affiliate_id": 19,
    "date_updated": "2026-03-02T23:45:24Z",
    "event_date": "2026-03-08T19:45:00Z",
    "format": "American",
    "period_id": 0
  }
}
```

## Heartbeat Handling

All WebSocket endpoints send heartbeat messages every **15 seconds** to keep the connection alive. Your client must handle these to avoid treating them as data updates.

```json theme={null}
{
  "meta": {
    "type": "heartbeat"
  },
  "data": {
    "now": "2026-02-12T18:30:00Z"
  }
}
```

<Warning>
  Heartbeats arrive every 15 seconds. If you do not receive one within 60 seconds (i.e., four missed heartbeats), the connection is likely stale. Close and reconnect.
</Warning>

### Heartbeat Handler Pattern

<CodeGroup>
  ```javascript Node.js 22+ (ws) theme={null}
  let lastHeartbeat = Date.now();

  ws.on("message", (raw) => {
    const msg = JSON.parse(raw.toString());

    if (msg.meta?.type === "heartbeat") {
      lastHeartbeat = Date.now();
      return; // Do not process as data
    }

    // Process actual price update
    handleUpdate(msg.data);
  });

  // Monitor heartbeat health
  setInterval(() => {
    const elapsed = Date.now() - lastHeartbeat;
    if (elapsed > 60_000) {
      console.warn("No heartbeat in 60s, reconnecting...");
      ws.close();
      // Trigger reconnection logic
    }
  }, 10_000);
  ```

  ```python Python theme={null}
  import time

  last_heartbeat = time.time()


  async def listen_with_heartbeat():
      global last_heartbeat

      async with websockets.connect(
          WS_URL, additional_headers={"X-TheRundown-Key": API_KEY}
      ) as ws:
          async for message in ws:
              msg = json.loads(message)

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

              # Check heartbeat staleness
              if time.time() - last_heartbeat > 60:
                  print("No heartbeat in 60s, reconnecting...")
                  break

              handle_update(msg["data"])
  ```
</CodeGroup>

## Reconnection Best Practices

WebSocket connections can drop due to network issues, server maintenance, or client-side timeouts. Always implement automatic reconnection.

### Recommended Strategy

1. **Exponential backoff** -- start at 1 second, double on each failure, cap at 30 seconds.
2. **Add jitter** -- randomize the delay slightly to avoid thundering herd reconnections.
3. **Reset backoff on success** -- once a connection is established and receives data, reset the delay to 1 second.
4. **Refetch REST state after reconnect** -- you may have missed updates during the disconnection. Fetch the latest state from the REST API to ensure consistency.

<CodeGroup>
  ```javascript JavaScript theme={null}
  const WebSocket = require("ws");

  function createReconnectingWebSocket(url, onMessage) {
    const apiKey = process.env.THERUNDOWN_API_KEY;
    if (!apiKey) throw Error('Set THERUNDOWN_API_KEY');

    let ws;
    let reconnectDelay = 1000;
    const MAX_DELAY = 30000;

    function connect() {
      ws = new WebSocket(url, {
        headers: { "X-TheRundown-Key": apiKey },
      });

      ws.on("open", () => {
        console.log("WebSocket connected");
        reconnectDelay = 1000; // Reset backoff
      });

      ws.on("message", (raw) => {
        const msg = JSON.parse(raw.toString());
        if (msg.meta?.type === "heartbeat") return;
        onMessage(msg.data);
      });

      ws.on("close", () => {
        const jitter = Math.random() * 1000;
        const delay = Math.min(reconnectDelay + jitter, MAX_DELAY);
        console.log(`WebSocket closed. Reconnecting in ${Math.round(delay)}ms...`);

        setTimeout(() => {
          reconnectDelay = Math.min(reconnectDelay * 2, MAX_DELAY);
          connect();
        }, delay);
      });

      ws.on("error", (error) => {
        console.error("WebSocket error:", error);
        ws.close(); // Triggers onclose -> reconnect
      });
    }

    connect();

    return {
      close: () => {
        reconnectDelay = MAX_DELAY + 1; // Prevent reconnection
        ws.close();
      },
    };
  }

  // Usage
  const connection = createReconnectingWebSocket(
    "wss://therundown.io/api/v2/ws/markets?sport_ids=4",
    (update) => {
      console.log(`Price update: event=${update.event_id} market=${update.market_id} price=${update.price}`);
    }
  );
  ```

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


  async def connect_with_reconnect(url, on_message):
      reconnect_delay = 1.0
      max_delay = 30.0

      while True:
          try:
              async with websockets.connect(
                  url, additional_headers={"X-TheRundown-Key": API_KEY}
              ) as ws:
                  print("WebSocket connected")
                  reconnect_delay = 1.0  # Reset backoff

                  async for message in ws:
                      msg = json.loads(message)
                      if msg.get("meta", {}).get("type") == "heartbeat":
                          continue
                      await on_message(msg["data"])

          except (websockets.ConnectionClosed, ConnectionError) as e:
              jitter = random.uniform(0, 1)
              delay = min(reconnect_delay + jitter, max_delay)
              print(f"WebSocket closed ({e}). Reconnecting in {delay:.1f}s...")
              await asyncio.sleep(delay)
              reconnect_delay = min(reconnect_delay * 2, max_delay)

          except Exception as e:
              print(f"Unexpected error: {e}")
              await asyncio.sleep(5)


  async def handle_update(update):
      print(f"Price update: event={update['event_id']} market={update['market_id']} price={update['price']}")


  # Run
  API_KEY = os.environ["THERUNDOWN_API_KEY"]
  WS_URL = "wss://therundown.io/api/v2/ws/markets?sport_ids=4"
  asyncio.run(connect_with_reconnect(WS_URL, handle_update))
  ```
</CodeGroup>

## Tips for Production

<AccordionGroup>
  <Accordion title="Filter aggressively to reduce traffic">
    Always specify `sport_ids` and `market_ids` in your WebSocket connection. Receiving updates for all sports and markets generates significant traffic that your client may not need.
  </Accordion>

  <Accordion title="Process messages asynchronously">
    Do not block the WebSocket message handler with slow operations (database writes, API calls). Queue updates and process them in a separate thread or task.
  </Accordion>

  <Accordion title="Use the REST API as source of truth after reconnection">
    After a reconnect, fetch the latest state from the REST API to fill any gaps. WebSocket updates are incremental -- if you miss one, your state may be stale.
  </Accordion>

  <Accordion title="Monitor connection health">
    Track heartbeat intervals and reconnection frequency. Alert if the connection drops repeatedly, which may indicate a network issue or an invalid API key.
  </Accordion>
</AccordionGroup>

## Next Steps

<CardGroup cols={2}>
  <Card title="Multiplexed WebSocket" icon="layer-group" href="/guides/multiplexed-websocket">
    Markets, scores, plays, game stats, and live game state over one connection
  </Card>

  <Card title="Building an Odds Screen" icon="display" href="/guides/building-odds-screen">
    Use WebSocket data in a real-time UI
  </Card>

  <Card title="Getting Live Odds" icon="signal" href="/guides/getting-live-odds">
    REST API for initial data load
  </Card>

  <Card title="Rate Limits" icon="gauge" href="/rate-limits">
    WebSocket does not count against REST limits
  </Card>

  <Card title="V1 to V2 Migration" icon="arrow-right" href="/guides/v1-to-v2-migration">
    Migrate from V1 WebSocket to V2
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.