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

> Build a live odds feed from zero to production in one page.

## When to use WebSocket vs polling

The simplest rule: **use WebSocket for live/in-play events, use polling for pre-match**.

| Scenario                   | Recommended approach             | Why                                                          |
| -------------------------- | -------------------------------- | ------------------------------------------------------------ |
| Live in-play odds          | **WebSocket**                    | Odds change every few seconds — polling would hammer the API |
| Pre-match monitoring       | `GET /api/odds` every 30–60 s    | Odds are stable; polling is simpler and bandwidth-efficient  |
| Value bet detection        | `GET /api/value-bets` every 30 s | Pre-calculated, no streaming needed                          |
| Dashboard with live ticker | **WebSocket**                    | Push-based; no unnecessary requests                          |

<Note>
  WebSocket is available on **all plans**. Free plan: 30 connections/day. **Pro plan: not included.** Pro+ RT and Pro+ Live: unlimited. Check your plan at [oddsstream.io/dashboard](https://oddsstream.io/dashboard).
</Note>

***

## Prerequisites

* An API key starting with `os_live_` ([get one here](https://oddsstream.io/signup))
* Free, Pro+ RT, or Pro+ Live plan (Pro plan does not include WebSocket)
* Node.js 18+ **or** Python 3.10+ depending on your stack

Store your key in an environment variable — never hardcode it:

```bash theme={null}
export ODDSSTREAM_API_KEY=os_live_YOUR_KEY
```

***

## Step 1: Get a short-lived token

WebSocket connections require a **short-lived token** (5-minute TTL) rather than your API key directly. This is because:

* Browsers cannot set custom headers on WebSocket connections
* The WS server is separate from the API auth gateway — the token is the signed credential that crosses this boundary

Exchange your API key for a token before every new connection:

<Tabs>
  <Tab title="Node.js">
    ```javascript theme={null}
    async function getWsToken() {
      const res = await fetch("https://oddsstream.io/api/stream/token", {
        method: "POST",
        headers: { "X-Api-Key": process.env.ODDSSTREAM_API_KEY },
      });
      if (!res.ok) throw new Error(`Token request failed: ${res.status}`);
      const { token } = await res.json();
      return token;
    }
    ```
  </Tab>

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

    API_KEY = os.environ["ODDSSTREAM_API_KEY"]

    async def get_ws_token() -> str:
        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"]
    ```
  </Tab>

  <Tab title="curl">
    ```bash theme={null}
    curl -X POST "https://oddsstream.io/api/stream/token" \
      -H "X-Api-Key: $ODDSSTREAM_API_KEY"
    # → { "token": "<token>", "expires_in": 300 }
    ```
  </Tab>
</Tabs>

***

## Step 2: Connect with the token

Pass the token as a `?token=` query parameter on the WebSocket URL:

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

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

    const token = await getWsToken();
    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}
    // NEVER expose your API key in browser code.
    // Get the token from your own backend server:
    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, websockets

    async def main():
        token = await get_ws_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:
                update = json.loads(message)
                print(update)

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

***

## Step 3: Filter the stream

Without filters, you receive **every** odds change across all sports and bookmakers — potentially thousands of messages per minute. Filter at connection time to only receive what you care about.

Append filters as additional query parameters after the token:

| Parameter     | Example              | Effect             |
| ------------- | -------------------- | ------------------ |
| `sport`       | `&sport=Football`    | Football odds only |
| `competition` | `&competition=EPL`   | EPL events only    |
| `bookmaker`   | `&bookmaker=Winamax` | Winamax odds only  |

Combine them freely:

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

<Tip>
  Start filtered. An unfiltered stream across all sports can deliver 50–200 messages per minute during peak hours. Filter to your sport and competition to keep message volume manageable.
</Tip>

***

## Step 4: Handle messages

Each message is a JSON object representing **one selection's odds changing**. One market update (e.g. a moneyline with 3 outcomes) 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"
}
```

**What each field means:**

| Field         | Plain English                                                  |
| ------------- | -------------------------------------------------------------- |
| `bookmaker`   | Which sportsbook changed their price                           |
| `match_name`  | The match (e.g. `"PSG - Lyon"`)                                |
| `competition` | Short code for the league (e.g. `LIG1` = Ligue 1)              |
| `market_type` | Type of bet: `moneyline`, `total`, `spread`, etc.              |
| `selection`   | Which outcome changed (e.g. `"PSG"`, `"Over 2.5"`)             |
| `odds`        | The new decimal price                                          |
| `period`      | `0` = full game/match, `1` = first half                        |
| `is_live`     | `true` if the match is currently in progress                   |
| `scraped_at`  | When this price was scraped — latency is typically 1–3 seconds |

A simple handler that builds a local price cache:

```javascript theme={null}
const prices = {}; // { "PSG - Lyon": { "Winamax/moneyline/PSG": 1.85, ... } }

ws.on("message", (data) => {
  const update = JSON.parse(data);
  if (update === "pong") return; // skip keepalive responses

  const { match_name, bookmaker, market_type, selection, odds } = update;
  const key = `${bookmaker}/${market_type}/${selection}`;

  if (!prices[match_name]) prices[match_name] = {};
  prices[match_name][key] = odds;

  console.log(`${match_name} | ${bookmaker} ${selection}: ${odds}`);
});
```

***

## Step 5: Keepalive

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

<Tabs>
  <Tab title="Node.js">
    ```javascript theme={null}
    ws.on("open", () => {
      const keepalive = setInterval(() => {
        if (ws.readyState === WebSocket.OPEN) ws.send("ping");
      }, 30_000);

      ws.on("close", () => clearInterval(keepalive));
    });

    ws.on("message", (data) => {
      if (data.toString() === "pong") return; // skip keepalive responses
      // 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>

***

## Step 6: Reconnect automatically

The server restarts for deployments. Your client must handle disconnects gracefully using **exponential backoff** — start with a 1-second retry delay and double it up to 30 seconds.

**Re-fetch the token on every reconnect attempt.** Tokens expire after 5 minutes, so the old token may be invalid when you reconnect.

<Tabs>
  <Tab title="Node.js (production class)">
    ```javascript theme={null}
    import WebSocket from "ws";

    class OddsStreamClient {
      constructor({ apiKey, filters = {}, onUpdate }) {
        this.apiKey = apiKey;
        this.filters = filters;
        this.onUpdate = onUpdate;
        this.reconnectDelay = 1000;
        this.keepaliveInterval = null;
        this.connect();
      }

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

      async buildUrl() {
        const token = await this.getToken();
        const params = new URLSearchParams({ token, ...this.filters });
        return `wss://api.oddsstream.io/api/stream?${params}`;
      }

      async connect() {
        let url;
        try {
          url = await this.buildUrl();
        } catch (e) {
          console.error("Failed to get token:", e.message);
          setTimeout(() => this.connect(), this.reconnectDelay);
          this.reconnectDelay = Math.min(this.reconnectDelay * 2, 30_000);
          return;
        }

        const ws = new WebSocket(url);

        ws.on("open", () => {
          console.log("OddsStream connected");
          this.reconnectDelay = 1000;
          this.keepaliveInterval = setInterval(
            () => ws.readyState === WebSocket.OPEN && ws.send("ping"),
            30_000
          );
        });

        ws.on("message", (data) => {
          const str = data.toString();
          if (str === "pong") return;
          try {
            this.onUpdate(JSON.parse(str));
          } catch (e) {
            console.error("Parse error:", e);
          }
        });

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

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

    // Usage
    const client = new OddsStreamClient({
      apiKey: process.env.ODDSSTREAM_API_KEY,
      filters: { sport: "Football", competition: "EPL" },
      onUpdate: (update) => {
        console.log(`${update.match_name} | ${update.selection}: ${update.odds}`);
      }
    });
    ```
  </Tab>

  <Tab title="Python (production)">
    ```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() -> str:
        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(filters: dict, on_update):
        """
        Connects to the OddsStream WebSocket with automatic reconnection.
        filters: dict of query params, e.g. {"sport": "Football", "competition": "EPL"}
        on_update: async callable that receives each update dict
        """
        from urllib.parse import urlencode
        base = "wss://api.oddsstream.io/api/stream"
        delay = 1

        while True:
            try:
                token = await get_token()
                params = urlencode({"token": token, **filters})
                uri = f"{base}?{params}"

                async with websockets.connect(uri) as ws:
                    delay = 1
                    print("OddsStream connected")

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

                    asyncio.create_task(keepalive())

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

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

    # Usage
    async def handle(update):
        print(f"{update['match_name']} | {update['selection']}: {update['odds']}")

    asyncio.run(stream({"sport": "Football", "competition": "EPL"}, handle))
    ```
  </Tab>
</Tabs>

***

## Complete working example

A minimal but complete app that connects, filters to EPL Football, maintains a live price table, and prints best available odds per selection.

<Tabs>
  <Tab title="Node.js">
    ```javascript theme={null}
    // live-odds.mjs
    // Usage: ODDSSTREAM_API_KEY=os_live_... node live-odds.mjs
    import WebSocket from "ws";

    const API_KEY = process.env.ODDSSTREAM_API_KEY;
    if (!API_KEY) throw new Error("ODDSSTREAM_API_KEY not set");

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

    // prices[match][selection] = { odds, bookmaker }
    const prices = {};

    function printBest(matchName) {
      const match = prices[matchName];
      if (!match) return;
      console.log(`\n=== ${matchName} ===`);
      for (const [sel, { odds, bookmaker }] of Object.entries(match)) {
        console.log(`  ${sel.padEnd(25)} ${odds.toFixed(2)} @ ${bookmaker}`);
      }
    }

    let reconnectDelay = 1000;

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

      ws.on("open", () => {
        reconnectDelay = 1000;
        console.log("Connected to OddsStream EPL feed");
        setInterval(() => ws.readyState === WebSocket.OPEN && ws.send("ping"), 30_000);
      });

      ws.on("message", (raw) => {
        const str = raw.toString();
        if (str === "pong") return;

        const u = JSON.parse(str);
        if (!prices[u.match_name]) prices[u.match_name] = {};

        const current = prices[u.match_name][u.selection];
        if (!current || u.odds > current.odds) {
          prices[u.match_name][u.selection] = { odds: u.odds, bookmaker: u.bookmaker };
          printBest(u.match_name);
        }
      });

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

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

    connect();
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    # live_odds.py
    # Usage: ODDSSTREAM_API_KEY=os_live_... python live_odds.py
    import asyncio, json, os, httpx, websockets
    from websockets.exceptions import ConnectionClosed

    API_KEY = os.environ.get("ODDSSTREAM_API_KEY")
    if not API_KEY:
        raise RuntimeError("ODDSSTREAM_API_KEY not set")

    async def get_token() -> str:
        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"]

    # prices[match_name][selection] = {"odds": float, "bookmaker": str}
    prices: dict[str, dict] = {}

    def print_best(match_name: str):
        match = prices.get(match_name, {})
        print(f"\n=== {match_name} ===")
        for sel, data in match.items():
            print(f"  {sel:<25} {data['odds']:.2f} @ {data['bookmaker']}")

    async def main():
        delay = 1

        while True:
            try:
                token = await get_token()
                uri = f"wss://api.oddsstream.io/api/stream?token={token}&sport=Football&competition=EPL"

                async with websockets.connect(uri) as ws:
                    delay = 1
                    print("Connected to OddsStream EPL feed")

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

                    async for message in ws:
                        if message == "pong":
                            continue
                        u = json.loads(message)
                        match = u["match_name"]
                        sel = u["selection"]

                        if match not in prices:
                            prices[match] = {}

                        current = prices[match].get(sel)
                        if not current or u["odds"] > current["odds"]:
                            prices[match][sel] = {"odds": u["odds"], "bookmaker": u["bookmaker"]}
                            print_best(match)

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

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

***

## Common errors

<AccordionGroup>
  <Accordion title="401 Unauthorized on token request">
    Your API key is missing or wrong. Check `ODDSSTREAM_API_KEY` is set and starts with `os_live_`. Try a simple test:

    ```bash theme={null}
    curl "https://oddsstream.io/api/sports" -H "X-Api-Key: $ODDSSTREAM_API_KEY"
    ```
  </Accordion>

  <Accordion title="403 on token request (websocket_not_on_plan)">
    Your plan doesn't include WebSocket streaming. **Pro plan** does not include WebSocket — upgrade to **Pro+ RT** or **Pro+ Live** at [oddsstream.io/pricing](https://oddsstream.io/pricing). The Free plan has a 30 connections/day limit.
  </Accordion>

  <Accordion title="429 on token request (rate_limit_exceeded)">
    Free plan: 30 WS token requests per day. Each connection attempt counts. Implement keepalive (Step 5) to avoid unnecessary reconnects. To remove the limit, upgrade to Pro+ RT or Pro+ Live.
  </Accordion>

  <Accordion title="Connection closes immediately (code 4001)">
    Your token is missing, expired, or invalid. Tokens expire after 5 minutes — always fetch a fresh token immediately before connecting. Never cache and reuse tokens across reconnects.
  </Accordion>

  <Accordion title="No messages arriving">
    If you connect but receive nothing, your filters may be too narrow. Try removing all filter params to connect unfiltered — if you see messages, your filter value is wrong. Use `GET /api/sports` to check valid sport names and `GET /api/bookmakers` for valid bookmaker names.
  </Accordion>

  <Accordion title="Messages lag behind / high latency">
    Normal scrape-to-push latency is 1–3 seconds. Higher latency usually means a bookmaker's scraper is running slowly. Check `scraped_at` in the message — if it's consistently >30 seconds old, contact support.
  </Accordion>
</AccordionGroup>

***

<CardGroup cols={2}>
  <Card title="WebSocket Reference" icon="plug" href="/api-reference/stream">
    Full technical spec for the stream endpoint.
  </Card>

  <Card title="Best Practices" icon="shield-check" href="/guides/best-practices">
    Production patterns for reliability and efficiency.
  </Card>
</CardGroup>
