Skip to main content
WebSocket is available on all plans. Free plan: 30 connections/day. Pro plan: not included. Pro+ RT and Pro+ Live: unlimited.

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.

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:
curl -X POST "https://oddsstream.io/api/stream/token" \
  -H "X-Api-Key: os_live_YOUR_KEY"
Response:
{ "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>
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));

Filter Parameters

Narrow what you receive by appending filters to the connection URL. Combine them freely.
ParameterExampleDescription
token?token=<token>Required. Short-lived auth token.
sport&sport=FootballOnly events for this sport
competition&competition=EPLOnly events for this competition code
bookmaker&bookmaker=WinamaxOnly 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.
{
  "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

FieldTypeDescription
bookmakerstringWhich bookmaker changed their price
match_namestringCanonical event name (e.g. "PSG - Lyon")
competitionstringCompetition code (e.g. LIG1 = Ligue 1)
sportstringSport name
market_typestringMarket type — see Market Types
selectionstringWhich outcome changed (e.g. "PSG", "Over 2.5")
oddsfloatNew decimal odds value
periodinteger0 = full time/game, 1 = first half
is_livebooleantrue if the match is currently in-play
scraped_atISO-8601 stringUTC 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".
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
});

Reconnection

The server restarts for deployments. Implement exponential backoff and re-fetch a token on each reconnect attempt.
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();

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

CodeReason
4001Invalid, 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 monitoringGET /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.