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

# WebSocket stream (subscriptions)

> Subscribe to the sports, leagues, events or markets you actually follow, and receive only those.

<Note>
  Real-time streams require the **Sharp** plan.
</Note>

`wss://api.apinn.io/ws` pushes odds movements like the [SSE stream](/en/concepts/streaming), with one difference that matters: the connection is **two-way**, so you can tell the server what you want. Without a subscription you receive the whole book — exactly what the SSE stream sends.

Pass your key in the `X-API-Key` header, or as `?key=YOUR_KEY` in the URL (a browser cannot set a header on a WebSocket connection).

```javascript Node.js theme={null}
import WebSocket from "ws";

const sock = new WebSocket("wss://api.apinn.io/ws?key=YOUR_KEY");

sock.on("open", () => {
  sock.send(JSON.stringify({ op: "subscribe", sports: [29], markets: ["moneyline"] }));
});

sock.on("message", (buf) => {
  const m = JSON.parse(buf);
  if (m.t === "snapshot") {
    // full current state of your subscription
    console.log(m.count, "lines");
  } else if (m.t === "delta") {
    // only the lines that changed, within your subscription
    for (const row of m.rows) console.log(row.event_id, row.market, row.odds1, row.odds2);
  }
});
```

## Subscribing

Send `{"op": "subscribe", ...}` with any combination of the filters below. They are cumulative: a line must match all the filters you set.

| Filter    | Type           | Example                                |
| --------- | -------------- | -------------------------------------- |
| `sports`  | sport ids      | `{"sports": [29, 33]}`                 |
| `leagues` | league ids     | `{"leagues": [2436]}`                  |
| `events`  | event ids      | `{"events": [1632682788]}`             |
| `markets` | market names   | `{"markets": ["moneyline", "totals"]}` |
| `periods` | period numbers | `{"periods": [0]}` — full time only    |

Subscribing again replaces the previous subscription and returns a fresh snapshot. `{"op": "unsubscribe"}` returns you to the full book, and `{"op": "ping"}` answers `{"t": "pong"}`.

## Server messages

| `t`        | Payload                                         | Meaning                                                                       |
| ---------- | ----------------------------------------------- | ----------------------------------------------------------------------------- |
| `hello`    | `{ subscription }`                              | Sent on connection and after each subscription change; echoes what is active. |
| `snapshot` | `{ count, rows }`                               | Full current state **of your subscription**, sent right after you subscribe.  |
| `delta`    | `{ rows, specials, removed, removed_specials }` | What changed since the previous message — and what disappeared.               |
| `pong`     | `{ ts }`                                        | Reply to `ping`.                                                              |

Every delta also carries a `seq`, incremented once per broadcast. A client with no subscription receives every number in order, so a gap means a lost message; a client with a subscription will normally see gaps (the broadcasts that did not concern it).

## Removals

`removed` and `removed_specials` list the series that no longer exist — a match that ended, a market that closed. **Delete them from your book.** A client that ignores them keeps serving odds on markets that are gone: measured over nine hours, that is around 2.5% of the book, and it grows.

Removed entries carry only what identifies the series, not prices: `event_id`, `period`, `market`, `line` for odds; `event_id`, `special_id`, `period`, `contestant_name`, `handicap` for specials. They are filtered by your subscription like everything else.

Rows use the same shape as `/api/odds`: `event_id`, `period`, `market`, `line`, the odds (`odds1`/`odds0`/`odds2`), the true odds (`todds*`), `status`, and `timestamp` — the time of the **last price change**, not the time we sent it.

## Why subscribe

Filtering is not just convenience. Measured on the live book, football alone is around 40% of all movements, moneyline on full time around 49%, and a single match is a rounding error. A client that follows a handful of matches goes from tens of thousands of lines an hour to a few dozen.

<Tip>
  Like the SSE stream, the connection counts as **1 request**, at the moment you connect. What you subscribe to changes the volume you receive, not the price.
</Tip>

<Note>
  Odds lines carry `event_id`, `period` and `market`, but not the sport or the league — those are resolved through the fixtures. An event that is not yet in the fixtures index will not match a `sports` or `leagues` filter; filter on `events` if you need it immediately.
</Note>
