How Often Do Odds APIs Update? Polling vs WebSockets in Python

Odds API Update Frequency - OddsPapi API Blog
How To Guides June 17, 2026

“How often does an odds API update?” is the first question every developer asks before wiring a feed into a bot — and the honest answer is: it depends on the market, the sport, and how you ask for the data. A pre-game moneyline three weeks out might sit still for hours. The same fixture, live and in-play, can reprice dozens of times a minute. If your code polls on a dumb fixed timer, you either hammer the API for data that hasn’t changed or you miss the move that mattered.

This guide explains how odds update frequency actually works, how to read the timestamps that tell you when a price last moved, and the two ways to consume updates — polling the REST endpoint vs. subscribing to a WebSocket push from 350+ bookmakers. All the code is tested against the live OddsPapi API.

Why Odds Update at All (and How Fast)

Bookmaker prices move for three reasons: money coming in on one side, the sharps (Pinnacle, SBOBet, Singbet) repricing their model, and in-play game events. The cadence is wildly different depending on the state of the market:

Market state Typical update cadence What’s driving it
Pre-game, days out Minutes to hours Early sharp money, opening-line discovery
Pre-game, last hour Seconds to minutes Steam, late money, lineup news
Live / in-play Sub-second to seconds Goals, possession, clock, momentum

To make this concrete: we polled a single live soccer fixture twice, 25 seconds apart. In that window, 36 individual outcomes repriced across the books on the fixture — Kalshi nudged the home price from 1.136 to 1.124, Pinnacle moved an Over/Under 3.5 line from 3.30 to 3.40. That’s the in-play reality. Your job as a developer is to capture those moves without drowning in redundant requests. The trick is timestamps.

The Two Timestamps That Tell You When a Price Moved

Every outcome in the OddsPapi /odds response carries two timestamps. Knowing the difference between them is the whole game:

  • bookmakerChangedAt — when the bookmaker itself last moved the price.
  • changedAt — when OddsPapi last observed a change to that outcome.

Here is a real, unedited outcome object — Pinnacle’s home price on a World Cup 2026 fixture, pulled live:

{
  "active": true,
  "betslip": null,
  "bookmakerOutcomeId": "home",
  "bookmakerChangedAt": "2026-06-05T08:46:15.557Z",   // book moved it
  "changedAt": "2026-06-05T08:46:15.947Z",            // OddsPapi saw the change
  "limit": 8775.0,
  "playerName": null,
  "price": 1.641,                                     // decimal odds
  "priceAmerican": "-156",                            // same price, American (string)
  "priceFractional": "25/39",                         // same price, fractional (string)
  "mainLine": true,
  "exchangeMeta": null
}

The feed pre-converts every price into decimal, American, and fractional — no converter to write. But the field that drives your update logic is changedAt: if it’s newer than the last time you saw it, the price moved. That single comparison is the core of an efficient odds consumer.

Polling vs. WebSocket: Two Ways to Get Updates

There are exactly two models for consuming odds updates. Most generic APIs only give you the first one.

Polling (REST) WebSocket (push)
How it works You ask “what’s the price now?” on a timer The server pushes the new price the moment it changes
Latency Up to your poll interval (e.g. 5s) Near real-time — you get the move as it lands
Wasted calls High — most polls return unchanged data Zero — you only receive actual changes
Best for Pre-game, scheduled scans, backtests Live in-play, arb/steam detection, trading
OddsPapi support /odds REST endpoint ✅ Real-time WebSocket on 350+ books

The rule of thumb: poll when the data is slow-moving, stream when it’s fast. Let’s build both.

Method 1: Detecting Updates by Polling (Python)

Step 1 — Authenticate

Auth is a query parameter, not a header. One key, every endpoint:

import requests, time

API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.oddspapi.io/v4"

# Smoke test
r = requests.get(f"{BASE_URL}/sports", params={"apiKey": API_KEY})
print(r.status_code)  # 200

Step 2 — Snapshot every price with its changedAt

The /odds response is deeply nested: bookmakerOdds → slug → markets → outcomes → players["0"]. We flatten it into a flat dict keyed by (book, market, outcome) so we can diff two snapshots cheaply:

def snapshot(fixture_id):
    """Return {(book, market, outcome): (changedAt, price)} for a fixture."""
    r = requests.get(f"{BASE_URL}/odds",
                     params={"apiKey": API_KEY, "fixtureId": fixture_id})
    books = r.json().get("bookmakerOdds", {})
    snap = {}
    for slug, book in books.items():
        for market_id, market in book.get("markets", {}).items():
            for outcome_id, outcome in market.get("outcomes", {}).items():
                price = outcome["players"]["0"]   # live odds: dict, single price
                key = (slug, market_id, outcome_id)
                snap[key] = (price.get("changedAt"), price.get("price"))
    return snap

Step 3 — Diff two polls to find what moved

An outcome moved if its changedAt is different between two polls. This is the entire detection mechanism — no guessing, no storing full price history:

fixture_id = "id1000078065454466"   # a live in-play fixture

before = snapshot(fixture_id)
time.sleep(25)                       # wait, then re-poll
after = snapshot(fixture_id)

moved = [k for k in before
         if k in after and before[k][0] != after[k][0]]

print(f"{len(moved)} outcomes moved in 25 seconds")
for slug, market_id, outcome_id in moved[:5]:
    old = before[(slug, market_id, outcome_id)][1]
    new = after[(slug, market_id, outcome_id)][1]
    print(f"  {slug} {market_id}/{outcome_id}: {old} -> {new}")

Run against a live fixture, this printed 36 outcomes moved in 25 seconds with lines like kalshi 101/101: 1.136 -> 1.124 and pinnacle 1012/1012: 3.3 -> 3.4. You’re now detecting real market movement with a six-line diff — the same primitive that powers a steam move detector or an arbitrage scanner.

Step 4 — Poll on a sane interval

How often should you poll? Match the interval to the market and respect the rate limit. The free tier has roughly a 0.88s cooldown between calls to the same endpoint, so a small sleep keeps you safe:

POLL_SECONDS = 5          # pre-game: 30-60s is plenty; in-play: 2-5s
last = {}

while True:
    current = snapshot(fixture_id)
    moves = [k for k in current
             if k in last and last[k][0] != current[k][0]]
    if moves:
        print(f"{len(moves)} prices changed")
        # ... feed moves into your alert / arb / model logic
    last = current
    time.sleep(POLL_SECONDS)   # never poll the same endpoint faster than ~1/sec

Polling is simple and perfect for pre-game scans, scheduled jobs, and backtests. But notice the inefficiency: at 5-second polls, most requests return data that hasn’t changed, and you’re still up to 5 seconds behind the market. For live trading, that gap is the difference between catching a steam move and chasing it. That’s what the WebSocket fixes.

Method 2: Real-Time Updates via WebSocket

Instead of asking “what’s the price now?” on a loop, you open one persistent connection and the server pushes each change the instant it happens — across all 350+ bookmakers, with no polling, no wasted calls, and no fixed-interval lag. You only ever receive actual updates.

This is the model you want for in-play arbitrage, steam detection, and any latency-sensitive trading logic. We cover the full WebSocket subscription flow — connecting, subscribing to fixtures, and parsing the push messages — in the dedicated WebSocket Odds API guide. The short version: it’s the difference between checking the scoreboard every few seconds and having the goals announced to you the moment they’re scored.

Your use case Recommended model
Daily line-shopping scan Polling, 30-60s
Pre-game value scanner Polling, 10-30s
Live in-play arbitrage WebSocket
Steam / sharp-money alerts WebSocket (or 2-5s polling)
Historical backtesting No polling — use free historical odds

The Honest Answer to “How Often Do Odds Update?”

There is no single number, and any API that gives you one is hiding something. Update frequency is a property of the market, not the feed: a quiet pre-game line might not move for an hour, while a live fixture reprices many times a minute. What a good odds API gives you is (1) accurate changedAt timestamps so you always know when a price last moved, and (2) a choice between polling and WebSocket push so you can match your consumption to your latency budget. OddsPapi gives you both, across 350+ books, on a free tier.

Get Your Free API Key

Stop polling blind. Read the changedAt timestamp, poll efficiently when you can, and stream via WebSocket when latency matters — all from one feed covering 350+ bookmakers including the sharps. Grab your free OddsPapi API key and start detecting line movement today.

Frequently Asked Questions

How often do odds APIs update?

It depends on the market state. Pre-game lines days out may update only every few minutes to hours; the final hour before kickoff updates every few seconds; live in-play markets can reprice sub-second to several times a minute. OddsPapi exposes a changedAt timestamp on every outcome so you always know exactly when a price last moved.

What’s the difference between polling and WebSocket for odds data?

Polling means repeatedly requesting the current price on a timer (REST /odds endpoint) — simple, but you’re limited by your poll interval and most requests return unchanged data. A WebSocket pushes each price change to you the instant it happens, giving near real-time updates with zero wasted calls. OddsPapi supports both.

How do I detect when an odds price changes in Python?

Compare the changedAt timestamp of each outcome between two polls. If changedAt is different, the price moved. Flatten the nested /odds response into a dict keyed by (book, market, outcome) and diff two snapshots — a six-line comparison is enough.

How fast can I poll the OddsPapi API?

The free tier has roughly a 0.88-second cooldown between calls to the same endpoint, so keep at least a ~1-second gap. For pre-game data, 30-60 second polls are plenty; for live data, use 2-5 second polls or switch to the WebSocket to avoid polling entirely.

What’s the difference between changedAt and bookmakerChangedAt?

bookmakerChangedAt is when the bookmaker itself last moved the price; changedAt is when OddsPapi last observed a change to that outcome. Use changedAt to drive your own update-detection logic.