Odds API WebSocket
The WebSocket feed delivers the exact same response format as the /odds endpoint, but in real time . Instead of polling, you receive updates instantly whenever odds change.
We recommend all clients move to WebSocket for efficiency and lower latency. It reduces request overhead, scales better, and gives you immediate updates for live markets.
Why Use WebSocket?
Real-Time Updates Receive odds changes instantly without polling
Lower Latency Sub-150ms updates for live markets
Reduced Overhead Single persistent connection vs repeated HTTP calls
Better Scaling Perfect for bots, live dashboards, and in-play betting apps
Access & Pricing
Add-on Feature : WebSocket access is available as an add-on. Subscribe through odds-api.io to enable it for your account.
Pricing: 2x the REST API price
Bookmakers: The WebSocket automatically sends updates for all bookmakers you have selected in your account. You can manage your selected bookmakers via the /bookmakers/selected/select endpoint or through the dashboard .
Connection Details
Endpoint:
wss://api.odds-api.io/v3/ws?apiKey=YOUR_API_KEY
Authentication:
API key passed as query parameter
One connection per API key
New connections automatically close older ones
Automatic cleanup keeps your feed stable
Filter Parameters
The markets parameter is required . You must specify which markets you want to receive.
Parameter Type Max Description marketscomma-separated 20 Required. Market names (e.g., ML,Spread,Totals)sportcomma-separated 10 Filter by sport slugs (e.g., football,basketball) leaguescomma-separated 20 Filter by league slugs (e.g., england-premier-league) eventIdscomma-separated 50 Filter by specific event IDs statussingle value - live or prematch
Using leagues or eventIds is recommended to reduce bandwidth. You cannot use both together.
Example Connection URLs
# All live football events with main markets only
wss://api.odds-api.io/v3/ws?apiKey =xxx & sport = football & status = live & markets = ML,Spread,Totals
# Specific leagues
wss://api.odds-api.io/v3/ws?apiKey =xxx & leagues = england-premier-league,spain-la-liga & markets = ML
# Specific events
wss://api.odds-api.io/v3/ws?apiKey =xxx & eventIds = 12345,67890,11111
# Multiple sports, prematch only
wss://api.odds-api.io/v3/ws?apiKey =xxx & sport = football,basketball,tennis & status = prematch
Welcome Message
Upon successful connection, you’ll receive a welcome message confirming your active filters:
{
"type" : "welcome" ,
"message" : "Connected to OddsAPI WebSocket" ,
"user_id" : "user123" ,
"bookmakers" : [ "Bet365" , "Pinnacle" ],
"sport_filter" : [ "Football" ],
"leagues_filter" : [ "england-premier-league" ],
"event_id_filter" : [],
"status_filter" : "live" ,
"market_filter" : [ "ML" , "SPREAD" , "TOTALS" ],
"connected_at" : "2026-01-15T21:00:00Z" ,
"warning" : "..."
}
Error Responses
If your connection parameters are invalid, you’ll receive a 400 Bad Request with one of these errors:
Error Message Cause Too many sports. Maximum 10 allowed.Exceeded sport limit Too many leagues. Maximum 20 allowed.Exceeded league limit Too many event IDs. Maximum 50 allowed.Exceeded eventIds limit Too many markets. Maximum 20 allowed.Exceeded market limit Cannot use both 'leagues' and 'eventIds' filters together.Mutual exclusion violated Invalid status filter. Use 'prematch' or 'live'Invalid status value
Update Types
Messages come in as JSON objects. Each has a type field:
Type Description createdNew match added updatedMatch or market changed deletedMatch removed no_marketsMatch exists but currently no markets available
Each message includes a seq field — a globally unique, monotonically increasing sequence number. Use this to detect gaps and to reconnect without missing updates (see Reconnection with Replay below).
{
"type" : "updated" ,
"seq" : 482917 ,
"timestamp" : 1723992773 ,
"id" : "63017989" ,
"bookie" : "SingBet" ,
"url" : "https://www.singbet.com/sports/football/match/63017989" ,
"markets" : [
{
"name" : "ML" ,
"updatedAt" : "2024-01-15T10:30:00Z" ,
"odds" : [
{
"home" : "1.85" ,
"draw" : "3.25" ,
"away" : "2.10" ,
"max" : 500
}
]
},
{
"name" : "Totals" ,
"updatedAt" : "2024-01-15T10:30:00Z" ,
"odds" : [
{
"hdp" : 2.5 ,
"over" : "1.85" ,
"under" : "2.10" ,
"max" : 500
}
]
}
]
}
Quick Start
JavaScript Example
const WebSocket = require ( 'ws' );
const apiKey = process . env . ODDS_API_KEY ;
// Build connection URL with filters
const params = new URLSearchParams ({
apiKey ,
markets: 'ML,Spread,Totals' ,
sport: 'football' ,
status: 'live'
});
const ws = new WebSocket ( `wss://api.odds-api.io/v3/ws? ${ params } ` );
ws . onopen = () => {
console . log ( "Connected to Odds-API WebSocket" );
};
ws . onclose = () => {
console . log ( "Disconnected from WebSocket" );
};
ws . onerror = ( err ) => {
console . error ( "WebSocket error:" , err );
};
ws . onmessage = ( event ) => {
const data = JSON . parse ( event . data );
if ( data . type === "welcome" ) {
console . log ( "Connected:" , data . message );
console . log ( "Active filters:" , {
sports: data . sport_filter ,
leagues: data . leagues_filter ,
status: data . status_filter ,
markets: data . market_filter
});
} else if ( data . type === "updated" ) {
console . log ( `Match ID: ${ data . id } ` );
console . log ( `Bookmaker: ${ data . bookie } ` );
console . log ( `Markets: ${ data . markets . length } ` );
// Process each market
data . markets . forEach ( market => {
console . log ( ` ${ market . name } :` , market . odds [ 0 ]);
});
}
};
Python Example
import websocket
import json
import os
from urllib.parse import urlencode
api_key = os.environ[ 'ODDS_API_KEY' ]
# Build connection URL with filters
params = urlencode({
'apiKey' : api_key,
'markets' : 'ML,Spread,Totals' ,
'sport' : 'football' ,
'status' : 'live'
})
def on_open ( ws ):
print ( "Connected to Odds-API WebSocket" )
def on_message ( ws , message ):
data = json.loads(message)
if data[ 'type' ] == 'welcome' :
print ( f "Connected: { data[ 'message' ] } " )
print ( f "Active filters: sports= { data[ 'sport_filter' ] } , "
f "status= { data[ 'status_filter' ] } , markets= { data[ 'market_filter' ] } " )
elif data[ 'type' ] == 'updated' :
print ( f "Match ID: { data[ 'id' ] } " )
print ( f "Bookmaker: { data[ 'bookie' ] } " )
print ( f "Markets: { len (data[ 'markets' ]) } " )
# Process each market
for market in data[ 'markets' ]:
print ( f " { market[ 'name' ] } : { market[ 'odds' ][ 0 ] } " )
def on_error ( ws , error ):
print ( f "Error: { error } " )
def on_close ( ws , close_status_code , close_msg ):
print ( "Disconnected from WebSocket" )
# Create WebSocket connection
ws = websocket.WebSocketApp(
f "wss://api.odds-api.io/v3/ws? { params } " ,
on_open = on_open,
on_message = on_message,
on_error = on_error,
on_close = on_close
)
# Run forever
ws.run_forever()
PHP Example
<? php
use WebSocket\ Client ;
$apiKey = getenv ( 'ODDS_API_KEY' );
$params = http_build_query ([
'apiKey' => $apiKey ,
'markets' => 'ML,Spread,Totals' ,
'sport' => 'football' ,
'status' => 'live'
]);
$client = new Client ( "wss://api.odds-api.io/v3/ws?{ $params }" );
while ( true ) {
try {
$data = json_decode ( $client -> receive (), true );
if ( $data [ 'type' ] === 'welcome' ) {
echo "Connected! Filters: " . implode ( ', ' , $data [ 'sport_filter' ]) . " \n " ;
} elseif ( $data [ 'type' ] === 'updated' ) {
echo "Match ID: { $data ['id']} at { $data ['bookie']} \n " ;
foreach ( $data [ 'markets' ] as $market ) {
echo " { $market ['name']}: " . json_encode ( $market [ 'odds' ][ 0 ]) . " \n " ;
}
}
} catch ( Exception $e ) {
echo "Error: " . $e -> getMessage () . " \n " ;
break ;
}
}
Handling Different Update Types
ws . onmessage = ( event ) => {
const data = JSON . parse ( event . data );
switch ( data . type ) {
case 'welcome' :
console . log ( `Connected: ${ data . message } ` );
console . log ( `User ID: ${ data . user_id } ` );
console . log ( `Bookmakers: ${ data . bookmakers . join ( ', ' ) } ` );
console . log ( `Filters: sports= ${ data . sport_filter } , status= ${ data . status_filter } ` );
if ( data . warning ) {
console . warn ( `Warning: ${ data . warning } ` );
}
break ;
case 'created' :
console . log ( `New match created: ${ data . id } at ${ data . bookie } ` );
// Add match to your database/UI
break ;
case 'updated' :
console . log ( `Match updated: ${ data . id } at ${ data . bookie } ` );
// Update existing match odds
updateMatchOdds ( data . id , data . bookie , data . markets );
break ;
case 'deleted' :
console . log ( `Match deleted: ${ data . id } at ${ data . bookie } ` );
// Remove match from your database/UI
break ;
case 'no_markets' :
console . log ( `No markets available for match: ${ data . id } ` );
// Handle temporarily unavailable markets
break ;
default :
console . log ( 'Unknown message type:' , data . type );
}
};
function updateMatchOdds ( matchId , bookmaker , markets ) {
markets . forEach ( market => {
console . log ( ` ${ market . name } updated at ${ market . updatedAt } :` , market . odds [ 0 ]);
// Update your UI or database here
// Example: store latest odds for comparison
const odds = market . odds [ 0 ];
if ( market . name === 'ML' ) {
console . log ( ` Home: ${ odds . home } , Draw: ${ odds . draw } , Away: ${ odds . away } ` );
} else if ( market . name === 'Totals' ) {
console . log ( ` Over ${ odds . hdp } : ${ odds . over } , Under: ${ odds . under } ` );
}
});
}
Advanced Implementation: Live Odds Tracker
class LiveOddsTracker {
constructor ( apiKey , options = {}) {
this . apiKey = apiKey ;
this . options = {
markets: 'ML,Spread,Totals' ,
sport: null ,
leagues: null ,
eventIds: null ,
status: null ,
... options
};
this . ws = null ;
this . matches = new Map ();
this . lastSeq = 0 ;
this . reconnectAttempts = 0 ;
this . maxReconnectAttempts = 5 ;
}
buildUrl () {
const params = new URLSearchParams ({ apiKey: this . apiKey });
if ( this . options . markets ) params . set ( 'markets' , this . options . markets );
if ( this . options . sport ) params . set ( 'sport' , this . options . sport );
if ( this . options . leagues ) params . set ( 'leagues' , this . options . leagues );
if ( this . options . eventIds ) params . set ( 'eventIds' , this . options . eventIds );
if ( this . options . status ) params . set ( 'status' , this . options . status );
if ( this . lastSeq > 0 ) params . set ( 'lastSeq' , String ( this . lastSeq ));
return `wss://api.odds-api.io/v3/ws? ${ params } ` ;
}
connect () {
this . ws = new WebSocket ( this . buildUrl ());
this . ws . onopen = () => {
this . reconnectAttempts = 0 ;
};
this . ws . onmessage = ( event ) => {
this . handleMessage ( JSON . parse ( event . data ));
};
this . ws . onclose = () => {
console . log ( 'WebSocket connection closed' );
this . reconnect ();
};
this . ws . onerror = ( error ) => {
console . error ( 'WebSocket error:' , error );
};
}
handleMessage ( data ) {
if ( data . type === 'welcome' ) {
this . onWelcome ( data );
return ;
}
if ( data . type === 'resync_required' ) {
console . log ( 'Resync required:' , data . reason );
// Rebuild state from REST snapshot, then reconnect
return ;
}
// Track sequence number for gap-free reconnection
if ( data . seq ) {
this . lastSeq = data . seq ;
}
const key = ` ${ data . id } - ${ data . bookie } ` ;
switch ( data . type ) {
case 'created' :
this . matches . set ( key , {
id: data . id ,
bookmaker: data . bookie ,
markets: data . markets ,
timestamp: data . timestamp
});
this . onMatchCreated ( data );
break ;
case 'updated' :
const existing = this . matches . get ( key );
if ( existing ) {
existing . markets = data . markets ;
existing . timestamp = data . timestamp ;
this . onMatchUpdated ( data , existing );
}
break ;
case 'deleted' :
this . matches . delete ( key );
this . onMatchDeleted ( data );
break ;
case 'no_markets' :
this . onNoMarkets ( data );
break ;
}
}
// Override these methods in your implementation
onWelcome ( data ) {
console . log ( `Connected: ${ data . message } ` );
console . log ( `Filters: sports= ${ data . sport_filter } , status= ${ data . status_filter } ` );
}
onMatchCreated ( data ) {
console . log ( `New match: ${ data . id } at ${ data . bookie } ` );
}
onMatchUpdated ( data , previousData ) {
console . log ( `Updated: ${ data . id } at ${ data . bookie } ` );
}
onMatchDeleted ( data ) {
console . log ( `Deleted: ${ data . id } at ${ data . bookie } ` );
}
onNoMarkets ( data ) {
console . log ( `No markets: ${ data . id } ` );
}
reconnect () {
if ( this . reconnectAttempts < this . maxReconnectAttempts ) {
this . reconnectAttempts ++ ;
const delay = Math . min ( 1000 * Math . pow ( 2 , this . reconnectAttempts ), 30000 );
console . log ( `Reconnecting in ${ delay } ms... (Attempt ${ this . reconnectAttempts } )` );
setTimeout (() => this . connect (), delay );
} else {
console . error ( 'Max reconnection attempts reached' );
}
}
getMatchOdds ( matchId , bookmaker ) {
return this . matches . get ( ` ${ matchId } - ${ bookmaker } ` );
}
getAllMatches () {
return Array . from ( this . matches . values ());
}
disconnect () {
if ( this . ws ) {
this . ws . close ();
}
}
}
// Usage
const tracker = new LiveOddsTracker ( process . env . ODDS_API_KEY , {
markets: 'ML,Spread,Totals' ,
sport: 'football' ,
status: 'live'
});
tracker . connect ();
React Integration
import { useEffect , useState , useRef } from 'react' ;
function useLiveOdds ( apiKey , options = {}) {
const [ odds , setOdds ] = useState ({});
const [ connected , setConnected ] = useState ( false );
const ws = useRef ( null );
useEffect (() => {
const params = new URLSearchParams ({ apiKey });
if ( options . markets ) params . set ( 'markets' , options . markets );
if ( options . sport ) params . set ( 'sport' , options . sport );
if ( options . leagues ) params . set ( 'leagues' , options . leagues );
if ( options . status ) params . set ( 'status' , options . status );
ws . current = new WebSocket ( `wss://api.odds-api.io/v3/ws? ${ params } ` );
ws . current . onmessage = ( event ) => {
const data = JSON . parse ( event . data );
if ( data . type === 'welcome' ) {
setConnected ( true );
} else if ( data . type === 'updated' || data . type === 'created' ) {
setOdds ( prev => ({
... prev ,
[ ` ${ data . id } - ${ data . bookie } ` ]: {
id: data . id ,
bookmaker: data . bookie ,
markets: data . markets
}
}));
} else if ( data . type === 'deleted' ) {
setOdds ( prev => {
const next = { ... prev };
delete next [ ` ${ data . id } - ${ data . bookie } ` ];
return next ;
});
}
};
ws . current . onclose = () => setConnected ( false );
return () => ws . current ?. close ();
}, [ apiKey , options . markets , options . sport , options . leagues , options . status ]);
return { odds , connected };
}
// Component
function LiveOddsDisplay () {
const { odds , connected } = useLiveOdds ( process . env . REACT_APP_ODDS_API_KEY , {
markets: 'ML,Spread,Totals' ,
sport: 'football' ,
status: 'live'
});
return (
< div >
< div className = { connected ? 'connected' : 'disconnected' } >
{ connected ? 'Connected' : 'Disconnected' }
</ div >
< div className = "matches" >
{ Object . values ( odds ). map ( match => (
< div key = { ` ${ match . id } - ${ match . bookmaker } ` } >
< h3 > Match # { match . id } - { match . bookmaker } </ h3 >
{ match . markets . map ( market => (
< div key = { market . name } >
< strong > { market . name } : </ strong > { JSON . stringify ( market . odds [ 0 ]) }
</ div >
)) }
</ div >
)) }
</ div >
</ div >
);
}
Reconnection with Replay
When your client disconnects and reconnects, you can resume from where you left off using the lastSeq parameter. The server will replay any messages you missed during the disconnection.
How It Works
Track the seq field from every message you receive
When reconnecting, pass the last seq you received as a query parameter: lastSeq=482917
The server sends a burst of missed messages before resuming the live stream
After the replay burst, live updates continue as normal with no gap
Connection URL with Replay
wss://api.odds-api.io/v3/ws?apiKey =xxx & markets = ML,Spread,Totals & lastSeq = 482917
Replay Behavior
Compacted latest-state replay : You receive the most recent state for each eventId:bookie combination, not every intermediate tick. If odds changed multiple times while you were disconnected, you get the final value only. This is correct for state sync; trading clients who need every tick should use /odds/movements per event.
The replay burst is delivered as a batch of messages immediately after the welcome message
Messages are filtered through your active filters (sport, leagues, markets, etc.) — you only receive updates relevant to your session
deleted and no_markets updates are replayed as latest state — if an event was deleted while you were disconnected, you will receive the deletion during replay
Replay data is retained for up to 24 hours
The burst is size-limited (512KB per frame, up to 100 messages per frame) and sent as fast as the connection allows
Replay Limits and Resync
If the replay cannot be served reliably, the server will not silently truncate. Instead, it sends a resync_required message and closes the connection. This happens when:
Too many replay candidates (reason: "replay_limit_exceeded") — the gap between your lastSeq and the current seq produced more updates than the server will process
Expired replay data (reason: "replay_window_expired") — one or more replay payloads have aged out of the retention window, which would leave gaps in the catch-up
{
"type" : "resync_required" ,
"reason" : "replay_limit_exceeded" ,
"last_seq" : 48291000 ,
"current_seq" : 48295000
}
When you receive resync_required, rebuild your state from REST:
Fetch a fresh snapshot from /odds, /odds/multi, or /odds/all with includeSeq=true
Read the X-OddsAPI-Seq response header
Reconnect to the WebSocket with lastSeq set to that value
You are now caught up with no gap
Example: Reconnect with Replay
class ReconnectingClient {
constructor ( apiKey , options = {}) {
this . apiKey = apiKey ;
this . options = options ;
this . lastSeq = 0 ;
}
buildUrl () {
const params = new URLSearchParams ({ apiKey: this . apiKey });
if ( this . options . markets ) params . set ( 'markets' , this . options . markets );
if ( this . options . sport ) params . set ( 'sport' , this . options . sport );
if ( this . lastSeq > 0 ) params . set ( 'lastSeq' , String ( this . lastSeq ));
return `wss://api.odds-api.io/v3/ws? ${ params } ` ;
}
connect () {
this . ws = new WebSocket ( this . buildUrl ());
this . ws . onmessage = ( event ) => {
const data = JSON . parse ( event . data );
// Handle resync_required — rebuild from REST snapshot
if ( data . type === 'resync_required' ) {
console . log ( 'Resync required:' , data . reason );
this . resyncFromRest ();
return ;
}
// Track seq from every update
if ( data . seq ) {
this . lastSeq = data . seq ;
}
// Process the message
this . handleMessage ( data );
};
this . ws . onclose = () => {
// Reconnect with lastSeq to get missed messages
setTimeout (() => this . connect (), 1000 );
};
}
async resyncFromRest () {
// Fetch fresh snapshot with seq cursor
const res = await fetch (
`https://api.odds-api.io/v3/odds/all?apiKey= ${ this . apiKey } &includeSeq=true`
);
const seq = res . headers . get ( 'X-OddsAPI-Seq' );
if ( seq ) {
this . lastSeq = parseInt ( seq , 10 );
}
// Rebuild local state from response...
const data = await res . json ();
this . rebuildState ( data );
// Reconnect with the snapshot seq
setTimeout (() => this . connect (), 100 );
}
}
REST to WebSocket Handoff
You can obtain a seq cursor from REST API responses by adding includeSeq=true to the snapshot odds endpoints (/odds, /odds/multi, /odds/all). The current sequence number is returned in the X-OddsAPI-Seq response header.
curl -H "Accept: application/json" \
"https://api.odds-api.io/v3/odds?apiKey=xxx&eventId=123&bookmakers=Bet365&includeSeq=true"
# Response header: X-OddsAPI-Seq: 482900
Use this seq value as lastSeq when opening your WebSocket connection to ensure no updates are missed between the REST snapshot and the live stream.
Best Practices
Use Filters to Reduce Bandwidth
Use leagues or eventIds filters when you only need specific data. This significantly reduces bandwidth and improves performance.
Implement Reconnection Logic
Use lastSeq for gap-free reconnection. Always implement exponential backoff as a fallback. The connection is stable, but networks can drop.
Handle welcome, created, updated, deleted, and no_markets to keep your data in sync.
One Connection Per API Key
New connections automatically close older ones. Don’t create multiple connections with the same API key.
Process Updates Asynchronously
If receiving many updates, process them asynchronously to avoid blocking your main thread.
Benefits Over REST API
Feature REST API WebSocket Latency 100-500ms (polling) <150ms (push) Request overhead Multiple HTTP requests Single persistent connection Real-time updates Manual polling Automatic push Bandwidth Higher (repeated headers) Lower (single connection) Server load Higher Lower Best for Batch requests Live updates
Get Access
Enable WebSocket Access Subscribe to WebSocket as an add-on through your odds-api.io account
Next Steps
Fetching Odds Learn about REST API odds fetching
Best Practices Optimize your implementation
Value Bets Identify profitable opportunities
API Reference Explore all API endpoints