Skip to main content

When to use WebSocket vs polling

The simplest rule: use WebSocket for live/in-play events, use polling for pre-match.
ScenarioRecommended approachWhy
Live in-play oddsWebSocketOdds change every few seconds — polling would hammer the API
Pre-match monitoringGET /api/odds every 30–60 sOdds are stable; polling is simpler and bandwidth-efficient
Value bet detectionGET /api/value-bets every 30 sPre-calculated, no streaming needed
Dashboard with live tickerWebSocketPush-based; no unnecessary requests
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.

Prerequisites

  • An API key starting with os_live_ (get one here)
  • 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:
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:
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;
}

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>
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));

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:
ParameterExampleEffect
sport&sport=FootballFootball odds only
competition&competition=EPLEPL events only
bookmaker&bookmaker=WinamaxWinamax 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
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.

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.
{
  "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:
FieldPlain English
bookmakerWhich sportsbook changed their price
match_nameThe match (e.g. "PSG - Lyon")
competitionShort code for the league (e.g. LIG1 = Ligue 1)
market_typeType of bet: moneyline, total, spread, etc.
selectionWhich outcome changed (e.g. "PSG", "Over 2.5")
oddsThe new decimal price
period0 = full game/match, 1 = first half
is_livetrue if the match is currently in progress
scraped_atWhen this price was scraped — latency is typically 1–3 seconds
A simple handler that builds a local price cache:
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".
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...
});

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.
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}`);
  }
});

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.
// 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();

Common errors

Your API key is missing or wrong. Check ODDSSTREAM_API_KEY is set and starts with os_live_. Try a simple test:
curl "https://oddsstream.io/api/sports" -H "X-Api-Key: $ODDSSTREAM_API_KEY"
Your plan doesn’t include WebSocket streaming. Pro plan does not include WebSocket — upgrade to Pro+ RT or Pro+ Live at oddsstream.io/pricing. The Free plan has a 30 connections/day limit.
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.
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.
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.
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.

WebSocket Reference

Full technical spec for the stream endpoint.

Best Practices

Production patterns for reliability and efficiency.