/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.
/bookmakers/selected/select endpoint or through the dashboard.
Connection Details
Endpoint:- API key passed as query parameter
- Up to two concurrent connections per API key
- Opening a third closes the oldest one
- Reconnecting within 2 seconds of that is refused with close code
1013, so back off a few seconds and retry - Automatic cleanup keeps your feed stable
Close Codes
When the server ends a connection it sends a close frame with one of these codes and a short reason:
A close without a frame (
1006 in most clients) means the network dropped or the connection stopped answering pings. Reconnect with backoff and lastSeq.
Compression (Required)
Your client must negotiatepermessage-deflate compression when it connects. Compressed feeds use 3-5x less bandwidth. Connections that do not offer it are refused with HTTP 426 Upgrade Required.
Most clients do this for you. Check the table before you pick a library:
To confirm, check the handshake request your client sends. It must include:
Filter Parameters
Using
leagues or eventIds is recommended to reduce bandwidth. You cannot use both together.Channels
Subscribe to one or more streams withchannels. Omitting it keeps the default odds stream, so existing connections are unchanged. Add scores and/or status for live event state:
odds(default):created,updated,deleted,no_marketsscores:scoremessages on every score changestatus:statusmessages onpending(new match added),live,settled,cancelled(carries the current scores, so you get the final result on settle)
score/status messages also carry an optional clock snapshot (same shape as GET /events/live) so you can re-anchor a match-clock ticker on every event without polling. See the Live Events guide.
score/status are event-level (no bookie/markets), best-effort (no seq, not replayed). When you reconnect with lastSeq and subscribe to status, the server resends the current status of every live event, and of events settled or cancelled in the last 12 hours, that match your filters. They arrive after the replay and before replay_complete, so a transition you missed while away still reaches you. See the WebSocket API reference for full details.
Example Connection URLs
Welcome Message
Upon successful connection, you’ll receive a welcome message confirming your active filters:Error Responses
If your connection parameters are invalid, you’ll receive a400 Bad Request with one of these errors:
Update Types
Messages come in as JSON objects. Each has atype field:
The
scores object in score and status messages has the same shape and period keys as /events; see Period Scores.
deleted and no_markets messages carry an empty markets array and are always delivered, even when you subscribe with a markets= filter, so filtered clients still learn when a match goes off the board.
They do respect your status=, sport= and leagues= filters, and carry the event’s date, so a removal for a pre-match fixture is not delivered on a status=live connection.
no_markets means the bookmaker has nothing for that event in your subscription: either no active markets at all, or none left in your markets= list. If the bookmaker still prices the event but has pulled every market you subscribe to, you get a no_markets for that event and bookmaker, so clear what you hold for it. It carries the seq of the update it stands in for.
You get it once when your markets disappear. Later updates that still carry none of your markets are not repeated, and the next updated brings them back when they return. A no_markets can also arrive for an event you have not seen yet on the connection; there is simply nothing to show for it.
Message Format
Each message includes aseq field: a global sequence number. On a connection, updates arrive in seq order and seq never decreases. The same update sent for several bookmakers shares one seq, so identify a frame by id, bookie and seq together, and don’t drop a frame just because its seq equals the previous one. The highest seq you received is the one to resume from (see Reconnection with Replay below). Seqs are shared by all subscribers, so yours are increasing but not consecutive: a jump between two updates is not a gap.
Rarely, during a brief server-side interruption, an odds update arrives without seq. Apply it as normal and leave your stored lastSeq unchanged. A reconnect from before that point gets current state instead of a replay, so nothing is lost.
Quick Start
JavaScript Example
Python Example
Use thewebsockets library (pip install websockets). It negotiates the required permessage-deflate compression by default. websocket-client does not support compression, so its connections are refused with HTTP 426.
PHP Example
Handling Different Update Types
Advanced Implementation: Live Odds Tracker
React Integration
Reconnection with Replay
When your client disconnects and reconnects, you can resume from where you left off using thelastSeq parameter. The server will replay any messages you missed during the disconnection.
How It Works
- Track the
seqfield from every live message that has one (live seqs only go up, so the last one is also the highest) - 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
Replay Behavior
- Compacted latest-state replay: You receive the most recent state for each
eventId:bookiecombination, 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/movementsper 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.), so you only receive updates relevant to your session
deletedandno_marketsupdates are replayed as latest state. If an event was deleted while you were disconnected, you will receive the deletion during replay- On a
status=prematchconnection, events that started while you were away arrive asno_marketsfor each of your bookmakers, in the replay or in the pushed current state, so their pre-match odds close. When neither is sent (you reconnect again shortly after being sent current state, or getresync_required), these closes are still sent for events that started after yourlastSeq, without aseq, beforereplay_complete; the rest of the board continues live. If they do not fit the per-connection limit,resync_required(reason: "snapshot_truncated") follows instead - With the
statuschannel, the currentstatusof live and recently finished events follows the replay (see Channels) - Replay data is retained for up to 24 hours. A
lastSeqolder than that gets your current state instead of a replay (see below) - Right after a server restart or deploy, the replay can also include the latest state of a few seconds of updates from just before your
lastSeq, carrying aseqat or below it. They are current state, so apply them as normal; it guards against an update the previous server instance sent you no copy of. Keep your storedlastSeqat the highest seq you have seen - Updates already covered by the replay are not sent again live; live updates after the replay all have a
seqabove the replay’s range
Knowing When Replay Is Done
AddreplayComplete=true to the connection URL to get one control message once the catch-up phase is over:
outcome is replayed, snapshot (current state was pushed instead, see below), resync_required (rebuild from REST), or live (no lastSeq was given, or you reconnected again shortly after being sent current state: only the pre-match closes and statuses above are caught up, then everything is live). through_seq is the highest seq the catch-up covered. The message is opt-in so that clients which do not expect new message types are unaffected.
- Each message is delivered as its own WebSocket frame - parse every frame with
JSON.parse(event.data), exactly like live updates - and the burst is 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 aresync_required message. This happens when:
- Too many replay candidates (
reason: "replay_limit_exceeded"): the gap between yourlastSeqand 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 - Dirty replay window (
reason: "replay_window_dirty"): the server had to drop or skip seqs after yourlastSeq, or sent updates withoutseq, so a clean replay cannot be guaranteed
last_seq is outside the replay window, the server first pushes the
current state of your subscription as ordinary created updates, then continues
streaming live on the same connection. No REST refetch or reconnect is needed -
your existing message handler applies them like any other update.
resync_required is sent when complete state cannot be guaranteed: there was
nothing cached to send, or the snapshot exceeded the per-connection limit
(reason: "snapshot_truncated"). In those cases refetch from REST and discard
the last_seq you stored so the next reconnect starts fresh.
It is also sent with reason: "snapshot_cooldown" when you keep reconnecting
with the same last_seq you were already served a snapshot for. No snapshot or
replay is sent then, so treat it like any other resync_required: rebuild from
REST and discard the stored last_seq. Do not simply adopt current_seq; the
updates between the two would be lost.
When you receive resync_required, rebuild your state from REST:
- Fetch a fresh snapshot from
/oddsor/odds/multiwithincludeSeq=true - Read the
X-OddsAPI-Seqresponse header - Reconnect to the WebSocket with
lastSeqset to that value - You are now caught up with no gap
Example: Reconnect with Replay
REST to WebSocket Handoff
You can obtain a seq cursor from REST API responses by addingincludeSeq=true to the snapshot odds endpoints (/odds, /odds/multi). The current sequence number is returned in the X-OddsAPI-Seq response header.
lastSeq when opening your WebSocket connection to ensure no updates are missed between the REST snapshot and the live stream.
Best Practices
Use a Client That Compresses
Use a Client That Compresses
permessage-deflate is required. Node ws, browsers and Python websockets negotiate it automatically; Python websocket-client and PHP textalk/websocket cannot, and are refused with HTTP 426. See Compression (Required).Use Filters to Reduce Bandwidth
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
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 All Message Types
Handle All Message Types
Handle
welcome, created, updated, deleted, and no_markets to keep your data in sync.Replace Markets, Never Merge Them
Replace Markets, Never Merge Them
Each
updated message contains the full current market set for that event and bookmaker, so overwrite what you have stored rather than merging into it.Merging is the most common integration mistake we see. A suspended market is dropped from the payload rather than marked, so if you merge, it stays in your data forever and you keep showing a price that is no longer available.Two Connections Per API Key
Two Connections Per API Key
Each API key can hold two connections at once, for example a primary and a standby. Opening a third closes the oldest, so reuse your connections instead of opening a new one per request. A new connection within 2 seconds of that is refused with close code
1013 (try again later), so wait a few seconds before reconnecting.Process Updates Asynchronously
Process Updates Asynchronously
If receiving many updates, process them asynchronously to avoid blocking your main thread.
Benefits Over REST API
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
Prices above a fair-odds estimate
API Reference
Explore all API endpoints