# Streaming

Keep one connection open and receive an event's prices whenever they change. Pro plans only.

> **Pro only**
> Streaming needs a Pro key. Other plans are refused with `403 Plan restriction`. See [Pricing](https://sockodds.com/pricing/).

## What the stream is

`wss://api.sockodds.com/v1/stream/odds` is a WebSocket that checks prices for you every 15 seconds and sends them when they change. Each update is the same body the matching [odds route](https://sockodds.com/docs/odds/) returns, so code that reads `/v1/odds` reads the stream too.

It saves you polling, not time: prices are the same ones the REST routes serve, refreshed every 15 seconds. It is pre-match first and not a low-latency in-play feed.

## Connect

Open the WebSocket with your key in the `Authorization: Bearer` header, as on every request. Choose exactly one of:

| Parameter | Streams |
| --- | --- |
| `event_id` | One event, like `GET /v1/events/{event_id}/odds`. |
| `league` | Up to `limit` events in one league, like `GET /v1/odds?league=...`. `limit` is 10 by default and at most 20. |

You can add `markets`, `bookmakers`, `regions`, `alternates` and `group_by`, which work exactly as they do on the [odds routes](https://sockodds.com/docs/odds/). Snapshots are grouped by market unless you add `group_by=bookmaker`. Narrow filters keep each update small.

Browsers cannot set headers on a WebSocket, and your key must stay on your server anyway, so connect from your own server and pass updates on to your pages. Node 22 and later has a WebSocket client built in that accepts headers.

This program connects to the Rugby League World Cup opener, prints a line for each event in the first snapshot and closes:

```javascript
const socket = new WebSocket("wss://api.sockodds.com/v1/stream/odds?event_id=ev_000ckk0nc1tg&markets=winner", {
  headers: { Authorization: `Bearer ${process.env.SOCKODDS_API_KEY}` },
});
socket.addEventListener("message", (message) => {
  const frame = JSON.parse(message.data);
  if (frame.type === "heartbeat") return; // nothing changed since the last snapshot
  if (frame.type === "error") {
    console.log(`${frame.status} ${frame.problem.title}: ${frame.problem.detail}`);
    return; // the server closes the connection next
  }
  // A snapshot: one event for event_id, a list of events for league.
  const events = Array.isArray(frame.data) ? frame.data : [frame.data];
  for (const event of events) {
    const prices = event.markets.flatMap((m) => m.outcomes.flatMap((o) => o.prices));
    const bookmakers = new Set(prices.map((p) => p.bookmaker));
    console.log(`${event.name}: ${prices.length} prices from ${bookmakers.size} bookmakers`);
  }
  socket.close();
});
```

Output (from a local copy of the API with invented test prices):

```text
Australia v New Zealand: 8 prices from 4 bookmakers
```

## Frames

Every message is one JSON object with a `type`:

| `type` | When | Body |
| --- | --- | --- |
| `snapshot` | On connect, then whenever the prices change. | The odds route's body, `data`, `meta` and `links`, with `type` added. |
| `heartbeat` | A refresh found nothing changed. | `at`: when the check ran, in UTC. |
| `error` | A refresh was refused. | `status` and `problem`, the same [problem object](https://sockodds.com/docs/errors/) a REST request gets. The connection closes straight after. |

The first snapshot arrives as soon as you connect. After that, each refresh sends either a new snapshot or a heartbeat. A snapshot is the full current state, not a list of changes, so replace what you held rather than merging into it.

The stream is read only. Do not send messages: any message from your side closes the connection.

## Limits

| Limit | Value |
| --- | --- |
| Refresh interval | 15 seconds |
| Open streams per key | 2 |
| Longest connection | 3,600 seconds |
| Events per league snapshot | 20 |
| Largest snapshot | 8,000,000 bytes |
| Largest unread backlog | 1,000,000 bytes |

A connection that would go over the per-key limit is refused before it opens with `429 Stream limit reached`. Bad parameters are refused with 400, and a plain HTTP request to the stream gets `426 Upgrade required`. A WebSocket client usually reports these only as a failed connection, so check your parameters and plan against the REST routes first.

## Billing

- The first snapshot, and every new snapshot after it, counts towards your allowance exactly like the matching odds request.
- A heartbeat is free: an unchanged refresh costs no objects.
- Every refresh, heartbeat or not, counts towards your per-minute request limit, so each open stream uses one request every 15 seconds. See [Limits](https://sockodds.com/docs/limits/).

## Close codes

| Code | Meaning | What to do |
| --- | --- | --- |
| `1000` | The connection reached its lifetime of 3,600 seconds. | Reconnect straight away for a fresh stream. |
| `1008` | Your key, plan or a limit changed (401, 403 or 429 on a refresh), or you sent a message. | Read the `error` frame. Fix the key or plan, or wait for the rate limit, before reconnecting. |
| `1009` | A snapshot was larger than 8,000,000 bytes. | Narrow `markets` or `bookmakers`, or lower `limit`, then reconnect. |
| `1011` | A refresh failed on our side. | Reconnect after a short wait, backing off if it repeats. |
| `1013` | Your client read too slowly and the backlog passed 1,000,000 bytes. | Process frames faster, or narrow the filters, then reconnect. |

## Reconnecting

Every stream ends, at the latest when it reaches its lifetime, so build reconnecting in from the start:

1. On close, look at the code. Reconnect at once after `1000`.
2. After `1011` or a dropped network, wait a second and reconnect, doubling the wait each time it fails, up to a minute or so.
3. After `1008`, `1009` or `1013`, change something first: the error frame or the table above says what. Reconnecting unchanged gets the same result.
4. Keep at most 2 streams open per key, and close one before opening another. A new connection's first snapshot is the full current state, so nothing needs replaying.

Want the movement between snapshots too? Read [History and snapshots](https://sockodds.com/docs/history/), or see [Live tracker](https://sockodds.com/docs/examples/live-tracker/) for a complete example.
