In-Play Odds API: Clock Nine Books Repricing 17 NBA Games

In-Play Odds API featured image
How To Guides November 10, 2026

In-play odds on an NBA game change every few seconds, and the books do not all change at the same rate. We pulled the moneyline history of nine books on all 17 NBA games played between 31 October and 3 November 2026, from tip-off to the final buzzer. When Pinnacle moved its price by 3 or more points of win probability, the other books moved the same way on 77% to 91% of those moves, a median of 0.6 to 3.6 seconds from Pinnacle’s move. The difference between books is how often they post a price, not how late they react.

This guide shows how to pull that in-play history in a few hundred kilobytes, measure each book’s move rate and timing against Pinnacle, and poll a live game. Every number below comes from live calls made on 3 November 2026.

In-Play Odds by Book: 17 NBA Games

The table uses market 111, the moneyline including overtime, on the home participant. “Matched” counts the Pinnacle moves of 3+ points that the book followed with a 1.5+ point move in the same direction, between 30 seconds before and 90 seconds after. The two prediction markets record snapshots far more often, so we list them apart from the sportsbooks.

Book Games Moves a minute in play (median) Snapshot gap in play (median) Pinnacle moves matched Median gap to Pinnacle’s move
Sportsbooks
pinnacle 17 2.42 10.4 s reference: 1,292 moves –
hardrockbet 16 2.46 8.3 s 89.0% 3.0 s
betmgm 17 2.10 11.1 s 90.2% 0.6 s
draftkings 17 2.04 11.9 s 90.9% 1.6 s
betrivers 17 1.84 12.4 s 80.8% 2.1 s
fanduel 17 1.79 15.1 s 89.6% 3.6 s
bet365 17 1.69 14.0 s 76.7% 1.6 s
Prediction markets
polymarket 17 4.18 1.1 s 83.4% 1.3 s
kalshi 15 2.78 1.1 s 82.9% 1.2 s

Read the timing column with the snapshot gap next to it. A sportsbook snapshot lands every 8 to 15 seconds, so a gap of 0.6 to 3.6 seconds means the books repriced on the same game event, not that one copied another. We do not rank books on who moved first: differences under 5 seconds sit inside the feed’s timestamp resolution. We also left Caesars out of the table, because it had too few in-play snapshots on several of the games to measure.

The move rate is the useful number. The Cleveland v Oklahoma City game ran 144 minutes from tip-off to the final buzzer. Over that time, a book at 2.4 moves a minute posts about 345 new moneyline prices, and one at 1.7 posts about 245. If you scan for a stale in-play price, the slower book is the one that sits behind the others for longer between updates. For the pre-match version of this question, where the gaps run to minutes, see the soft book lag study.

Step 1: Find a Played Game and Its Real Tip-Off

The scheduled startTime is not the tip-off. NBA fixtures carry trueStartTime and trueEndTime, the real start and end of play, and those two fields bound the in-play window. Sign up at oddspapi.io for a free key; it goes in the apiKey query parameter.

import time
import requests
from datetime import datetime

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

def get(endpoint, **params):
    """GET with the key as a query parameter; wait and retry on a 429."""
    while True:
        r = requests.get(f"{BASE}/{endpoint}", params={"apiKey": API_KEY, **params})
        if r.status_code != 429:
            return r
        time.sleep(5.0)

def ts(iso):
    return datetime.fromisoformat(iso.replace("Z", "+00:00")).timestamp()

# A played NBA game (tournamentId 132) and its real tip-off and final-buzzer times
games = get("fixtures", tournamentId=132, **{"from": "2026-11-02", "to": "2026-11-04"}).json()
game = next(g for g in games if g["statusName"] == "Finished"
            and g["participant1Name"] == "Cleveland Cavaliers")
tip, buzzer = ts(game["trueStartTime"]), ts(game["trueEndTime"])
print(game["fixtureId"], game["participant1Name"], "v", game["participant2Name"])
print("in play:", game["trueStartTime"][11:19], "to", game["trueEndTime"][11:19], "UTC")

# Output:
# id1100013273719484 Cleveland Cavaliers v Oklahoma City Thunder
# in play: 00:11:00 to 02:35:21 UTC

Step 2: Pull In-Play History for One Outcome

/historical-odds returns every market for every book you name unless you narrow it. For three books on this game, the unfiltered response was 157 MB. Pass outcomeId and it returns that one outcome for up to three books: about 210 KB for the same call. The marketId and markets parameters do not narrow it.

# One outcome, three books: outcomeId cuts the response from 157 MB to about 210 KB
time.sleep(1.0)
hist = get("historical-odds", fixtureId=game["fixtureId"],
           bookmakers="pinnacle,draftkings,fanduel",
           outcomeId=111).json()                 # 111 = participant 1 to win (incl. overtime)
time.sleep(4.6)

def in_play(slug):
    snaps = hist["bookmakers"][slug]["markets"]["111"]["outcomes"]["111"]["players"]["0"]
    snaps = sorted(snaps, key=lambda s: s["createdAt"])
    before = [s for s in snaps if ts(s["createdAt"]) < tip][-1:]   # the price at tip-off
    return before + [s for s in snaps if tip <= ts(s["createdAt"]) <= buzzer]

minutes = (buzzer - tip) / 60
for slug in ("pinnacle", "draftkings", "fanduel"):
    seq = in_play(slug)
    live = [s for s in seq if s["active"]]
    moves = sum(1 for a, b in zip(live, live[1:]) if a["price"] != b["price"])
    suspended = sum(min(ts(b["createdAt"]), buzzer) - max(ts(a["createdAt"]), tip)
                    for a, b in zip(seq, seq[1:] + [{"createdAt": game["trueEndTime"]}])
                    if not a["active"])
    print(f"{slug:<11} {moves / minutes:5.2f} moves a minute   suspended {100 * suspended / (buzzer - tip):4.1f}% of game time")

# Output:
# pinnacle     2.67 moves a minute   suspended 15.6% of game time
# draftkings   2.90 moves a minute   suspended  3.9% of game time
# fanduel      2.45 moves a minute   suspended  0.8% of game time

Books suspend and reopen their prices during play. On this game the suspended share ran from 0.8% to 15.6% of game time across the three books, so filter on the price-level active flag before you compare prices or count moves.

Step 3: Time Each Book Against Pinnacle’s Moves

# When Pinnacle moves 3+ points of win probability, how close in time does each book move?
def prob_series(slug):
    return [(ts(s["createdAt"]), 1 / s["price"]) for s in in_play(slug) if s["active"]]

pin = prob_series("pinnacle")
jumps = [(t, 1 if p > p0 else -1) for (t0, p0), (t, p) in zip(pin, pin[1:]) if abs(p - p0) >= 0.03]
print(f"Pinnacle made {len(jumps)} moves of 3+ points in play\n")

for slug in ("draftkings", "fanduel"):
    book, lags = prob_series(slug), []
    for t, sign in jumps:
        base = [p for tb, p in book if tb <= t - 1]
        if not base:
            continue
        # first same-direction move of 1.5+ points, from 30 s before to 90 s after Pinnacle's
        follow = [tb for tb, p in book if t - 30 <= tb <= t + 90 and (p - base[-1]) * sign >= 0.015]
        if follow:
            lags.append(follow[0] - t)
    lags.sort()
    print(f"{slug:<11} matched {len(lags)} of {len(jumps)} within 90 s, median gap {lags[len(lags) // 2]:+.1f} s")

# Output:
# Pinnacle made 150 moves of 3+ points in play
#
# draftkings  matched 134 of 150 within 90 s, median gap +1.3 s
# fanduel     matched 135 of 150 within 90 s, median gap +5.3 s

Pinnacle is the reference here because it is the sharp price on most boards, not because it is the fastest. The 30-second look-back catches books that recorded the same play a few seconds earlier. The 90-second limit stops a later, unrelated move from counting as a match.

Step 4: Poll a Live Game

History reconstructs a game after the fact. During the game, poll /odds and print each change with its changedAt timestamp. We ran this on a live game at 03:38 UTC on 3 November 2026:

# Live: poll one in-play NBA game for 60 seconds and print every moneyline change
from datetime import timedelta, timezone

now = datetime.now(timezone.utc)
time.sleep(1.0)
window = {"from": (now - timedelta(hours=4)).strftime("%Y-%m-%dT%H:%M:%SZ"),
          "to": (now + timedelta(days=1)).strftime("%Y-%m-%d")}
live = [g for g in get("fixtures", tournamentId=132, **window).json() if g["statusName"] == "Live"]
if not live:
    raise SystemExit("no NBA game in play right now")
game = live[0]
print(game["participant1Name"], "v", game["participant2Name"], "(live)\n")

last = {}
for _ in range(12):                                  # 12 polls, 5 s apart
    time.sleep(5.0)
    board = get("odds", fixtureId=game["fixtureId"],
                bookmakers="pinnacle,draftkings,fanduel,polymarket").json()["bookmakerOdds"]
    for slug, book in board.items():
        p = book.get("markets", {}).get("111", {}).get("outcomes", {}).get("111", {}).get("players", {}).get("0")
        if not p:
            continue
        key = (p["price"], p.get("active"))
        if last.get(slug) != key:
            last[slug] = key
            state = "live" if p.get("active") is not False else "suspended"
            print(f"{p['changedAt'][11:19]}  {slug:<11} {p['price']:>6}  {state}")

Output (Houston Rockets v Boston Celtics, live, first 14 changes):

Houston Rockets v Boston Celtics (live)

03:38:39  draftkings   1.174  live
03:39:08  fanduel       1.14  live
03:35:40  pinnacle     1.123  suspended
03:39:21  polymarket   1.205  live
03:39:31  pinnacle     1.196  live
03:39:32  polymarket   1.176  live
03:39:35  draftkings   1.125  live
03:39:34  fanduel       1.12  live
03:39:33  pinnacle     1.147  live
03:39:55  polymarket   1.163  live
03:40:01  pinnacle      1.19  live
03:40:00  polymarket   1.205  live
03:40:03  draftkings   1.167  live
03:40:04  fanduel       1.14  live

Between 03:39:31 and 03:39:35 UTC, all four books moved the Rockets price within the same 4 seconds. Polling every 5 seconds is enough to see that. If you need each change as it happens instead of on your poll, use the push feed: see the WebSocket odds API guide. For in-play soccer, where bet365 reprices a goal in seconds, see the bet365 live odds post. For the wider in-play market list, see the live betting API guide.

What to Build With In-Play Timing

  • A stale-price scanner: after a Pinnacle move of 3+ points, flag any book that has not moved within your polling interval. The table above tells you which books post least often.
  • An exchange-versus-sportsbook monitor: Polymarket and Kalshi post prices about every second. Compare their price to the sportsbooks between sportsbook updates. The Polymarket and Kalshi guide covers their order books.
  • A suspension tracker: log when each book’s active flag goes false and back to true. It marks the moments each book pulls its price, without a play-by-play feed.

For the NBA pre-match board, see the NBA opening night post and the live NBA odds page.

Frequently Asked Questions

Which odds API has in-play NBA odds?

OddsPapi returns in-play NBA odds from the /v4/odds endpoint, with Pinnacle, DraftKings, FanDuel, BetMGM, bet365, Polymarket and Kalshi on the same fixture ID. The /v4/historical-odds endpoint keeps the in-play snapshots after the game, on the free plan.

How fast do sportsbooks update in-play NBA odds?

On 17 NBA games between 31 October and 3 November 2026, the sportsbooks recorded a new moneyline snapshot every 8 to 15 seconds at the median during play. When Pinnacle moved 3 or more points of win probability, each book, on the moves it matched, moved in the same direction a median of 0.6 to 3.6 seconds from Pinnacle’s move.

Do Polymarket and Kalshi update faster than sportsbooks in play?

Their snapshots arrive more often: a median of 1.1 seconds apart in play, against 8 to 15 seconds for the sportsbooks. Polymarket posted 4.18 moneyline moves a minute and Kalshi 2.78, against 1.69 to 2.46 for the sportsbooks.

Why does my in-play history call return so much data?

Without a filter, /historical-odds returns every market for every book you name. Three books on one NBA game came back at 157 MB. Pass outcomeId to keep one outcome: the same call with outcomeId=111 returned about 210 KB.

How do I tell when an in-play price is suspended?

Read the active flag on each price. In play, books suspend and reopen their prices many times a game, so filter on active before you compare prices or count moves.

Get Started

One API key reads in-play odds from Pinnacle, the US sportsbooks, Polymarket and Kalshi on one fixture ID, and keeps every in-play snapshot in free historical data. 300+ bookmakers, with push delivery on the WebSocket feed.

Get Your Free API Key