> ## 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 updates and live market data.

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 is enabled on real-time API tiers by default. 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 |
| V1 (Legacy) | `wss://therundown.io/api/v1/ws`         | Legacy format line updates                     |

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

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

### Filter Parameters

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

| Parameter       | Type   | Description                                  |
| --------------- | ------ | -------------------------------------------- |
| `key`           | string | **Required.** Your API key                   |
| `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?key=YOUR_API_KEY&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

<CodeGroup>
  ```javascript JavaScript theme={null}
  const API_KEY = "YOUR_API_KEY";

  const ws = new WebSocket(
    `wss://therundown.io/api/v2/ws/markets?key=${API_KEY}&sport_ids=4&market_ids=1,2,3`
  );

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

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

    // 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.onerror = (error) => {
    console.error("WebSocket error:", error);
  };

  ws.onclose = (event) => {
    console.log(`WebSocket closed: code=${event.code} reason=${event.reason}`);
  };
  ```

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

  API_KEY = "YOUR_API_KEY"
  WS_URL = (
      f"wss://therundown.io/api/v2/ws/markets"
      f"?key={API_KEY}&sport_ids=4&market_ids=1,2,3"
  )


  async def listen():
      async with websockets.connect(WS_URL) 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

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

### Filter Parameters

| Parameter       | Type   | Description                      |
| --------------- | ------ | -------------------------------- |
| `key`           | string | **Required.** Your API key       |
| `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 JavaScript theme={null}
  let lastHeartbeat = Date.now();

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

    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) 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}
  function createReconnectingWebSocket(url, onMessage) {
    let ws;
    let reconnectDelay = 1000;
    const MAX_DELAY = 30000;

    function connect() {
      ws = new WebSocket(url);

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

      ws.onmessage = (event) => {
        const msg = JSON.parse(event.data);
        if (msg.meta?.type === "heartbeat") return;
        onMessage(msg.data);
      };

      ws.onclose = () => {
        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.onerror = (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?key=${API_KEY}&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 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) 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
  WS_URL = f"wss://therundown.io/api/v2/ws/markets?key={API_KEY}&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 URL. 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="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>
