Live Odds API: Real-Time Odds over WebSocket
A live odds API sends your code a bookmaker's price the moment it changes. Odds-API.io streams pre-match and in-play odds from 450+ bookmakers across 34 sports over one WebSocket connection, with live scores, match status and the match clock on the same stream.
WebSocket streaming is an add-on for any paid plan. REST is included in every plan.
- Bookmakers
- 450+
- Sports
- 34
- Latency
- <150ms
- Replay window
- 24h
Why WebSocket Instead of Polling
Polling asks for odds on a timer and gets mostly unchanged data back. A WebSocket stays open and only carries the prices that moved, so you see a change sooner and move far less data.
| Feature | REST polling | WebSocket |
|---|---|---|
| How changes reach you | You ask again on a timer | Pushed as soon as a price changes |
| Latency | Your poll interval plus the request | <150ms |
| Connections | A new HTTP request for every poll | One connection that stays open |
| Withdrawn markets | Re-read the event to notice them | Explicit deleted and no_markets messages |
| Best for | Scripts, backfills and quick lookups | Live betting apps, in-play models and arbitrage scanners |
Connect in a Few Lines
Open wss://api.odds-api.io/v3/ws with your API key and the markets you want. The first message is a welcome that confirms your filters, then odds arrive as they change.
// npm i ws
import WebSocket from 'ws'
const API = 'https://api.odds-api.io/v3'
const apiKey = process.env.ODDS_API_KEY
const markets = 'ML,Spread,Totals'
const eventIds = '63017989,63017990' // up to 10, for example from GET /v3/events/live
const bookmakers = 'Bet365,Unibet' // the bookmakers selected on your account
const board = new Map()
let lastSeq = 0
let needsRebuild = false
function connect() {
const params = new URLSearchParams({ apiKey, markets, eventIds, channels: 'odds,scores,status' })
if (lastSeq) params.set('lastSeq', String(lastSeq))
const ws = new WebSocket(`wss://api.odds-api.io/v3/ws?${params}`)
ws.on('message', (raw) => {
const msg = JSON.parse(raw)
if (msg.type === 'resync_required') {
needsRebuild = true
return ws.close()
}
if (msg.seq > lastSeq) lastSeq = msg.seq
const key = `${msg.id}:${msg.bookie}`
if (msg.type === 'created' || msg.type === 'updated') board.set(key, msg.markets)
if (msg.type === 'deleted' || msg.type === 'no_markets') board.delete(key)
if (msg.type === 'score') console.log(msg.id, msg.scores.home, msg.scores.away)
})
ws.on('error', (err) => console.error('WebSocket error:', err.message || err.code))
ws.on('close', async () => {
if (needsRebuild) lastSeq = await rebuildFromRest()
setTimeout(connect, 2000)
})
}
// Replay was not possible: reload the subscription from REST and resume from its seq
async function rebuildFromRest() {
for (let attempt = 1; ; attempt++) {
try {
const params = new URLSearchParams({ apiKey, eventIds, bookmakers, markets, includeSeq: 'true' })
const res = await fetch(`${API}/odds/multi?${params}`)
const seq = Number(res.headers.get('X-OddsAPI-Seq'))
if (!res.ok || !(seq > 0)) throw new Error(`snapshot not usable: ${res.status}`)
const events = await res.json()
board.clear()
for (const event of events) {
for (const [bookie, odds] of Object.entries(event.bookmakers)) board.set(`${event.id}:${bookie}`, odds)
}
needsRebuild = false
return seq
} catch (err) {
console.error(`${err.message}, retry ${attempt}`)
await new Promise((resolve) => setTimeout(resolve, Math.min(30000, 1000 * 2 ** attempt)))
}
}
}
connect()# pip install websockets
import asyncio
import json
import os
from urllib.parse import urlencode
import websockets
params = urlencode({
"apiKey": os.environ["ODDS_API_KEY"],
"markets": "ML,Spread,Totals",
"leagues": "england-premier-league",
"status": "live",
})
async def main():
async with websockets.connect(f"wss://api.odds-api.io/v3/ws?{params}") as ws:
async for message in ws:
msg = json.loads(message)
if msg["type"] == "updated":
for market in msg["markets"]:
print(msg["id"], msg["bookie"], market["name"], market["odds"][0])
asyncio.run(main())Compression is required
Your client must offer permessage-deflate when it connects. It cuts bandwidth 3 to 5 times, and a connection without it is refused with HTTP 426. Most libraries handle it for you:
| Platform | Library | Compression |
|---|---|---|
| Node.js | ws | On by default |
| Browser | native WebSocket | On by default |
| Python | websockets | On by default |
| Python | websocket-client | Not supported, use websockets |
| PHP | amphp/websocket-client | On by default (needs zlib) |
| PHP | textalk/websocket | Not supported, use amphp/websocket-client |
| Go | gorilla/websocket | Set EnableCompression = true |
| .NET | ClientWebSocket | Set DangerousDeflateOptions (.NET 6+) |
Message Types
Every message is a JSON object with a type. Odds messages use the same format as the /v3/odds endpoint, so one parser covers both.
created- A bookmaker added an event you are subscribed to
updated- Odds changed. The message carries the full current set of your markets for that event and bookmaker
deleted- The event was removed, or its bookmaker went down
no_markets- The event is still listed but none of your markets are open
score- The live score changed (scores channel)
status- The event went pending, live, settled or cancelled, with the current score (status channel)
clock- A match clock snapshot about every 30 seconds for each live event with a running clock
Replace, never merge
A suspended market is left out of the next updated message rather than flagged. Overwrite what you hold for that event and bookmaker with each message, and a pulled price disappears from your app on its own.
{
"type": "updated",
"seq": 482917,
"timestamp": 1723992773,
"id": "63017989",
"bookie": "Bet365",
"markets": [
{
"name": "ML",
"updatedAt": "2026-11-08T15:42:10Z",
"odds": [{ "home": "1.85", "draw": "3.25", "away": "2.10" }]
},
{
"name": "Totals",
"updatedAt": "2026-11-08T15:42:10Z",
"odds": [{ "hdp": 2.5, "over": "1.85", "under": "2.10" }]
}
]
}Channels and Filters
Choose the streams you want with channels, then narrow them with query parameters so you only receive the events you show. You cannot combine leagues and eventIds.
Channels
odds- The default.
created,updated,deletedandno_marketsfor the bookmakers selected on your account scoresscoremessages on every score change, plusclocksnapshotsstatusstatusmessages for pending, live, settled and cancelled, plusclocksnapshots
Filters
markets- Required for the odds channel. Up to 40 market names, such as
ML,Spread,Totals sport- Up to 10 sports, such as
football,basketball leagues- Up to 20 league slugs, such as
england-premier-league eventIds- Up to 50 event IDs
statuslivefor in-play only,prematchfor pre-match only
channels is an allowlist. Send channels=odds,scores,status to keep odds next to scores; channels=scores,status alone stops the odds.
Reconnect Without Gaps
Networks drop. Every odds message carries a seq number, and the feed uses it to catch you up after a reconnect instead of making you start over.
- 1
Keep the last seq
Store the highest
seqyou have received. Seqs only go up on a connection, but they are shared by all subscribers, so a jump is not a gap. - 2
Reconnect with lastSeq
Add
lastSeqto the connection URL. You get the latest state of every event and bookmaker that changed while you were away, then the live stream continues. - 3
Know when you are live
Add
replayComplete=trueto get onereplay_completemessage when the catch-up is done. Replay data is kept for up to 24 hours. - 4
Rebuild if asked
If a clean replay is not possible you get
resync_required. Fetch a snapshot from/v3/oddswithincludeSeq=trueand reconnect with the seq from theX-OddsAPI-Seqheader.
Each API key can hold two connections at once, for example a primary and a standby. More are available on request.
Live Scores and Match Clock
In-play odds make more sense next to the score. Subscribe to the scores and status channels on the same connection, or read the current state over REST.
- GET /v3/events/live
- Every event in play right now, with the score and a
clockobject. Filter by sport - clock
minute,playedSeconds,period,runningandstatusDetail, plus injury time in football and the server in tennis- Period scores
- Halves, quarters, sets, overtime and penalties under one set of keys for every sport
- Ticking clock
- Count forward from
playedSecondswhilerunningis true, and reset to the server value on every snapshot
REST Fallback: /odds and /odds/updated
Every plan includes the REST API, and it serves the same live odds. Use it for snapshots, or poll it where a socket does not fit.
- GET /v3/odds
- A full snapshot of one event from the bookmakers you choose.
/v3/odds/multitakes up to 10 events in one request - GET /v3/odds/updated
- A true delta: only the markets that changed since
since, which can be at most 90 seconds old. Pass theX-Next-Sinceresponse header assinceon the next call to continue with no gap - includeSeq=true
- Adds the current stream position as
X-OddsAPI-Seq, so you can hand off from a REST snapshot to the WebSocket without missing an update
A delta cannot tell you a market was withdrawn, so a polling client should re-read events with /v3/odds now and then. The WebSocket sends deleted and no_markets instead.
What the Live Odds Feed Covers
- Bookmakers
- 450+, including Bet365, DraftKings, FanDuel, BetMGM, Betfair Exchange and regional books in Europe, Asia, Australia and Latin America
- Sports
- 34, including football, basketball, tennis, American football, baseball, ice hockey, cricket and esports
- In-play
- Live odds from every bookmaker that prices in play, refreshed about every second
- Markets
- Moneyline, spreads, totals, Asian handicap, corners, cards and player props, depending on the bookmaker
- Latency
- <150ms for updates pushed over the WebSocket
- Format
- The same JSON as
/v3/odds, over WebSocket and REST
Plans and WebSocket Access
Every paid plan includes the REST API with live odds. WebSocket streaming is an add-on you can add to any plan.
- The add-on costs the plan price again, so 2× the plan price in total, monthly or yearly
- The stream carries every bookmaker selected on your account, so the plan decides how many books you get
- Scores, status and the match clock come on the same connection
- Cancel any month
Live Odds API FAQ
What is a live odds API?
A live odds API gives your code the current betting odds for events in play, as JSON, and keeps them up to date as bookmakers move their prices. Odds-API.io streams live and pre-match odds from 450+ bookmakers over a WebSocket, and serves the same odds over REST.
How fast are the live odds?
WebSocket updates arrive with <150ms latency, and in-play odds refresh about every second. Polling REST adds your poll interval on top, which is why we recommend WebSocket for in-play apps.
What happens if my WebSocket connection drops?
Reconnect with the lastSeq parameter set to the last seq you received. The server replays the latest state of everything that changed while you were away, for gaps of up to 24 hours, then continues live. If a clean replay is not possible it sends resync_required, and you rebuild from a REST snapshot.
Which WebSocket clients can I use?
Any client that supports permessage-deflate compression, which is required. Node.js ws, browser WebSocket and Python websockets do it by default. Python websocket-client and PHP textalk/websocket cannot, and are refused with HTTP 426. In Go, set EnableCompression on gorilla/websocket.
Does the feed include live scores and the match clock?
Yes. Add channels=odds,scores,status to the connection URL to get score changes, status changes and a match clock snapshot about every 30 seconds next to the odds. GET /v3/events/live returns the same scores and clock over REST.
How do I get WebSocket access?
Pick a paid plan and add WebSocket streaming. It costs the plan price again and streams every bookmaker selected on your account, with two connections per API key. The REST API, including live odds and /v3/odds/updated, is part of every plan.
Stream Live Odds Today
Pick a plan, add WebSocket streaming and open one connection for live odds from the 450+ bookmakers on the feed.
Cancel anytime · Same JSON over REST and WebSocket