# 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

[See Plans](https://odds-api.io/pricing) · [Read the WebSocket Guide](https://docs.odds-api.io/guides/websockets)

## 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.

```javascript
// 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()
```

```python
# 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.

```json
{
  "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`, `deleted` and `no_markets` for the bookmakers selected on your account
- scores: `score` messages on every score change, plus `clock` snapshots
- status: `status` messages for pending, live, settled and cancelled, plus `clock` snapshots

### 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
- status: `live` for in-play only, `prematch` for 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 `seq` you 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 `lastSeq` to 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=true` to get one `replay_complete` message 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/odds` with `includeSeq=true` and reconnect with the seq from the `X-OddsAPI-Seq` header.

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 `clock` object. Filter by sport
- clock: `minute`, `playedSeconds`, `period`, `running` and `statusDetail`, 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 `playedSeconds` while `running` is 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/multi` takes 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 the `X-Next-Since` response header as `since` on 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

[Compare plans](https://odds-api.io/pricing)

## 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.

## Explore More

- [WebSocket guide](https://docs.odds-api.io/guides/websockets)
- [Live events and match clock guide](https://docs.odds-api.io/guides/live-events)
- [Fetching odds over REST](https://docs.odds-api.io/guides/fetching-odds)
- [Sports Betting API](https://odds-api.io/sports-betting-api)
- [How an odds feed API works](https://odds-api.io/blog/odds-feed-api)
- [Historical Odds API](https://odds-api.io/historical-odds-api)
- [Pricing](https://odds-api.io/pricing)

## Stream Live Odds Today

Pick a plan, add WebSocket streaming and open one connection for live odds from the 450+ bookmakers on the feed.

[See Plans](https://odds-api.io/pricing) · [Read the WebSocket Guide](https://docs.odds-api.io/guides/websockets)
