Skip to main content
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:
Authentication:
  • 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 negotiate permessage-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

The markets parameter is required for the odds channel (the default). It is not needed when subscribing only to scores/status. Check valid names with GET /v3/markets?sport=<sport>.
Using leagues or eventIds is recommended to reduce bandwidth. You cannot use both together.

Channels

Subscribe to one or more streams with channels. Omitting it keeps the default odds stream, so existing connections are unchanged. Add scores and/or status for live event state:
channels is an allowlist: you receive only the channels you list. To keep odds, include odds (e.g. channels=odds,scores,status); channels=scores,status alone stops odds delivery.
  • odds (default): created, updated, deleted, no_markets
  • scores: score messages on every score change
  • status: status messages on pending (new match added), live, settled, cancelled (carries the current scores, so you get the final result on settle)
Live 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 a 400 Bad Request with one of these errors:

Update Types

Messages come in as JSON objects. Each has a type 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.
Replace, don’t merge. Every updated message carries the complete set of markets we currently have for that bookmaker and event, not only the ones that changed. Replace your stored markets for that event with what arrives.If you merge incoming markets into what you already hold, a market that gets suspended or withdrawn disappears from the feed but never disappears from your copy. Suspended markets are removed from the payload rather than flagged, so absence is the signal. Merging leaves you quoting prices that are no longer offered.This applies to individual lines too. When a total is decided in play (say the 0.5 line once a goal is scored), there is no per-line “suspended” event: the line is simply missing from the next message for that event and bookmaker. A replace-based client drops it automatically; no timeout heuristics are needed.

Message Format

Each message includes a seq 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 the websockets 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

This sample needs permessage-deflate, which is why it uses amphp/websocket-client v2 (composer require amphp/websocket-client). It negotiates compression by default when PHP’s zlib extension is loaded. textalk/websocket cannot compress, so its connections are refused with HTTP 426.

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 the lastSeq parameter. The server will replay any messages you missed during the disconnection.

How It Works

  1. Track the seq field from every live message that has one (live seqs only go up, so the last one is also the highest)
  2. When reconnecting, pass the last seq you received as a query parameter: lastSeq=482917
  3. The server sends a burst of missed messages before resuming the live stream
  4. 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: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.), so 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
  • On a status=prematch connection, events that started while you were away arrive as no_markets for 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 get resync_required), these closes are still sent for events that started after your lastSeq, without a seq, before replay_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 status channel, the current status of live and recently finished events follows the replay (see Channels)
  • Replay data is retained for up to 24 hours. A lastSeq older 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 a seq at 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 stored lastSeq at 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 seq above the replay’s range

Knowing When Replay Is Done

Add replayComplete=true to the connection URL to get one control message once the catch-up phase is over:
Everything after it is live. 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 a resync_required message. 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
  • Dirty replay window (reason: "replay_window_dirty"): the server had to drop or skip seqs after your lastSeq, or sent updates without seq, so a clean replay cannot be guaranteed
When your 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:
  1. Fetch a fresh snapshot from /odds or /odds/multi with includeSeq=true
  2. Read the X-OddsAPI-Seq response header
  3. Reconnect to the WebSocket with lastSeq set to that value
  4. 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 adding includeSeq=true to the snapshot odds endpoints (/odds, /odds/multi). The current sequence number is returned in the X-OddsAPI-Seq response header.
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

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 leagues or eventIds filters when you only need specific data. This significantly reduces bandwidth and improves performance.
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.
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.
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.
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