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
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.
// 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);
import asyncio, json, os, websocketsasync 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())
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:
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.
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.
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...
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.
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.
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.
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.
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.
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.
WebSocket Reference
Full technical spec for the stream endpoint.
Best Practices
Production patterns for reliability and efficiency.