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

# WebSocket Stream

> Real-time odds push via WebSocket — no polling required.

<Note>
  WebSocket is available on **all plans**. Free plan: 30 connections/day. Pro plan: not included. Pro+ RT and Pro+ Live: unlimited.
</Note>

## WS /api/stream

Connect once and receive odds change events pushed by the server as bookmakers update their lines. Latency is typically **1–3 seconds** from scrape to push.

For a full walkthrough building a production live feed, see the [WebSocket Setup Guide](/guides/websocket-setup).

***

## Authentication: two-step token exchange

WebSocket connections cannot carry custom headers in browsers, and long-lived connections cannot use the same auth mechanism as short-lived HTTP requests. OddsStream uses a **short-lived token** (5-minute TTL) obtained from the REST API:

**Step 1 — Get a token:**

```bash theme={null}
curl -X POST "https://oddsstream.io/api/stream/token" \
  -H "X-Api-Key: os_live_YOUR_KEY"
```

Response:

```json theme={null}
{ "token": "<short-lived-token>", "expires_in": 300 }
```

**Step 2 — Connect with the token:**

```
wss://api.oddsstream.io/api/stream?token=<short-lived-token>
```

The token expires after **5 minutes**. Fetch a new one before each connection (including reconnects). If the token is missing, expired, or invalid the server closes the connection immediately with code `4001`.

### Why a token, not X-Api-Key directly?

* Browsers can't set custom headers on WebSocket connections
* The WS server (Railway) is separate from the auth gateway (Next.js/Vercel) — the token is the signed credential that crosses this boundary
* Short TTL limits exposure if a token is intercepted

***

## Connection URL

```
wss://api.oddsstream.io/api/stream?token=<token>
```

<Tabs>
  <Tab title="Node.js">
    ```javascript theme={null}
    import WebSocket from "ws"; // npm install ws

    async function getToken(apiKey) {
      const res = await fetch("https://oddsstream.io/api/stream/token", {
        method: "POST",
        headers: { "X-Api-Key": apiKey },
      });
      if (!res.ok) throw new Error(`Token request failed: ${res.status}`);
      const { token } = await res.json();
      return token;
    }

    const token = await getToken(process.env.ODDSSTREAM_API_KEY);
    const ws = new WebSocket(`wss://api.oddsstream.io/api/stream?token=${token}`);

    ws.on("open", () => console.log("Connected"));
    ws.on("message", (data) => console.log(JSON.parse(data)));
    ws.on("error", (err) => console.error("Error:", err.message));
    ws.on("close", (code) => console.log("Disconnected:", code));
    ```
  </Tab>

  <Tab title="Browser">
    ```javascript theme={null}
    // Get the token from your own server (never expose X-Api-Key in browser code)
    const { token } = await fetch("/your-backend/ws-token").then(r => r.json());

    const ws = new WebSocket(`wss://api.oddsstream.io/api/stream?token=${token}`);

    ws.onopen = () => console.log("Connected");
    ws.onmessage = (e) => console.log(JSON.parse(e.data));
    ws.onerror = (e) => console.error("Error:", e);
    ws.onclose = (e) => console.log("Disconnected:", e.code);
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    import asyncio, json, os, httpx, websockets

    API_KEY = os.environ["ODDSSTREAM_API_KEY"]

    async def get_token():
        async with httpx.AsyncClient() as client:
            r = await client.post(
                "https://oddsstream.io/api/stream/token",
                headers={"X-Api-Key": API_KEY},
            )
            r.raise_for_status()
            return r.json()["token"]

    async def main():
        token = await get_token()
        uri = f"wss://api.oddsstream.io/api/stream?token={token}"

        async with websockets.connect(uri) as ws:
            print("Connected")
            async for message in ws:
                print(json.loads(message))

    asyncio.run(main())
    ```
  </Tab>
</Tabs>

***

## Filter Parameters

Narrow what you receive by appending filters to the connection URL. Combine them freely.

| Parameter     | Example              | Description                                                     |
| ------------- | -------------------- | --------------------------------------------------------------- |
| `token`       | `?token=<token>`     | **Required.** Short-lived auth token.                           |
| `sport`       | `&sport=Football`    | Only events for this sport                                      |
| `competition` | `&competition=EPL`   | Only events for this competition code                           |
| `bookmaker`   | `&bookmaker=Winamax` | Only events from this bookmaker                                 |
| `game_id`     | `&game_id=<uuid>`    | Only changes for one specific game (UUID from `GET /api/games`) |

```
wss://api.oddsstream.io/api/stream?token=<token>&sport=Football&competition=EPL
wss://api.oddsstream.io/api/stream?token=<token>&sport=Basketball&bookmaker=Unibet.fr
```

**Unfiltered connections** receive all odds changes across all sports and bookmakers — useful for data ingestion pipelines but can deliver 50–200 messages/minute during peak hours.

***

## Message Format

Each push message is a JSON object representing **one selection's odds changing**. One market update (e.g. a 3-way moneyline) produces 3 separate messages — one per selection.

```json theme={null}
{
  "bookmaker": "Winamax",
  "match_name": "PSG - Lyon",
  "competition": "LIG1",
  "sport": "Football",
  "market_type": "moneyline",
  "selection": "PSG",
  "odds": 1.85,
  "period": 0,
  "is_live": false,
  "scraped_at": "2026-04-20T14:30:12Z"
}
```

### Message Fields

| Field         | Type            | Description                                                                |
| ------------- | --------------- | -------------------------------------------------------------------------- |
| `bookmaker`   | string          | Which bookmaker changed their price                                        |
| `match_name`  | string          | Canonical event name (e.g. `"PSG - Lyon"`)                                 |
| `competition` | string          | Competition code (e.g. `LIG1` = Ligue 1)                                   |
| `sport`       | string          | Sport name                                                                 |
| `market_type` | string          | Market type — see [Market Types](/api-reference/introduction#market-types) |
| `selection`   | string          | Which outcome changed (e.g. `"PSG"`, `"Over 2.5"`)                         |
| `odds`        | float           | New decimal odds value                                                     |
| `period`      | integer         | `0` = full time/game, `1` = first half                                     |
| `is_live`     | boolean         | `true` if the match is currently in-play                                   |
| `scraped_at`  | ISO-8601 string | UTC timestamp of the scrape that triggered this push                       |

***

## Keepalive

The server drops idle connections after **\~5 minutes**. Send the string `"ping"` every 30 seconds. The server responds with `"pong"`.

<Tabs>
  <Tab title="JavaScript">
    ```javascript theme={null}
    ws.on("open", () => {
      const interval = setInterval(
        () => ws.readyState === WebSocket.OPEN && ws.send("ping"),
        30_000
      );
      ws.on("close", () => clearInterval(interval));
    });

    ws.on("message", (raw) => {
      if (raw.toString() === "pong") return; // ignore keepalive responses
      const update = JSON.parse(raw.toString());
      // handle update
    });
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    async def keepalive(ws):
        while True:
            await asyncio.sleep(30)
            await ws.send("ping")

    async with websockets.connect(uri) as ws:
        asyncio.create_task(keepalive(ws))
        async for message in ws:
            if message == "pong":
                continue
            update = json.loads(message)
            # handle update
    ```
  </Tab>
</Tabs>

***

## Reconnection

The server restarts for deployments. Implement **exponential backoff** and re-fetch a token on each reconnect attempt.

<Tabs>
  <Tab title="JavaScript">
    ```javascript theme={null}
    let delay = 1000;

    async function connect() {
      const token = await getToken(process.env.ODDSSTREAM_API_KEY);
      const ws = new WebSocket(
        `wss://api.oddsstream.io/api/stream?token=${token}&sport=Football`
      );

      ws.on("open", () => {
        delay = 1000; // reset on success
        console.log("Connected");
        setInterval(() => ws.send("ping"), 30_000);
      });

      ws.on("message", (raw) => {
        if (raw.toString() === "pong") return;
        handleUpdate(JSON.parse(raw.toString()));
      });

      ws.on("close", (code) => {
        console.log(`Disconnected (${code}), reconnecting in ${delay}ms`);
        setTimeout(connect, delay);
        delay = Math.min(delay * 2, 30_000);
      });

      ws.on("error", (e) => console.error("WS error:", e.message));
    }

    connect();
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    import asyncio, json, os, httpx, websockets
    from websockets.exceptions import ConnectionClosed

    API_KEY = os.environ["ODDSSTREAM_API_KEY"]

    async def get_token():
        async with httpx.AsyncClient() as client:
            r = await client.post(
                "https://oddsstream.io/api/stream/token",
                headers={"X-Api-Key": API_KEY},
            )
            r.raise_for_status()
            return r.json()["token"]

    async def stream():
        uri_base = "wss://api.oddsstream.io/api/stream?sport=Football"
        delay = 1

        while True:
            try:
                token = await get_token()
                async with websockets.connect(f"{uri_base}&token={token}") as ws:
                    delay = 1
                    print("Connected")

                    async def ping():
                        while True:
                            await asyncio.sleep(30)
                            await ws.send("ping")
                    asyncio.create_task(ping())

                    async for message in ws:
                        if message == "pong":
                            continue
                        print(json.loads(message))

            except (ConnectionClosed, OSError) as e:
                print(f"Disconnected: {e}. Retrying in {delay}s")
                await asyncio.sleep(delay)
                delay = min(delay * 2, 30)

    asyncio.run(stream())
    ```
  </Tab>
</Tabs>

***

## Connection lifecycle

```
Client                                   Server
  │                                          │
  ├── POST /api/stream/token (X-Api-Key) ──► │  Next.js auth gateway
  ◄── { token, expires_in: 300 } ─────────  │
  │                                          │
  ├── WS upgrade (?token=<token>) ────────►  │  Railway WS server
  ◄── 101 Switching Protocols ────────────   │
  │                                          │
  ◄── {"bookmaker": "Winamax", ...} ──────   │  odds change
  ◄── {"bookmaker": "Betsson", ...} ──────   │  odds change
  │                                          │
  ├── "ping" ───────────────────────────►    │  every 30s
  ◄── "pong" ──────────────────────────      │
  │                                          │
  ├── (close / reconnect) ─────────────►     │  re-fetch token first
```

***

## Close codes

| Code   | Reason                                                                   |
| ------ | ------------------------------------------------------------------------ |
| `4001` | Invalid, missing, or expired token — fetch a new one before reconnecting |

***

## Notes

* **Scrape frequency varies** — 5–30 seconds per bookmaker. Don't expect sub-second latency.
* **Individual selection updates** — one message per changed odds line, not grouped by market. A 3-way moneyline update = 3 messages.
* **WS does not consume HTTP rate limit quota** — streaming is billed separately by plan.
* **Token TTL** — tokens expire 5 minutes after issue. Re-fetch before each new connection.
* **Free plan** — 30 WS connections per day. Each connection counts (even reconnects), so implement keepalive to avoid unnecessary reconnects.
* **For pre-match monitoring** — `GET /api/odds` polled every 30 seconds is more bandwidth-efficient than WS during low-change periods.
* **For live in-play monitoring** — WebSocket is strongly preferred; in-play odds change every 5–10 seconds.
