World Series Odds API: Price the Next Game 65 Hours Early

World Series Odds API
How To Guides October 28, 2026

If you search for a World Series odds API, you want the moneyline on every game of the series, from more than one book, as JSON. On 23 October 2026 at 13:35 UTC, eleven hours before the first pitch of Brewers v Rays, OddsPapi listed seven series fixtures. Game 1 carried 273 books. Game 2 carried Kalshi and the two Polymarket feeds. No sportsbook had posted it yet.

To check whether that gap is normal, we pulled the price history of all 29 played 2026 postseason games and found the pattern on every series. Pinnacle, DraftKings and FanDuel post the next game after the previous one ends: the first of the three arrived a median 41 minutes after the last out, and never later than 82 minutes. Kalshi listed 16 of those 19 follow-on games before the previous game had even finished, a median 64 hours ahead. If you want a line on Game 5 while Game 4 is still a day away, you read it from a prediction market, then watch for the sportsbooks to arrive.

Why a scraper sees an empty series

A scraper that polls a sportsbook’s MLB page sees one World Series game at a time. Games 2 to 7 do not exist on that page until the book posts them, so your model has nothing to price and your alert has nothing to compare. When the book does post, it posts in the hour after the last out, which is the hour you are least likely to be watching.

OddsPapi lists every scheduled game of the series as its own fixture, and attaches every book that quotes each game: 350+ bookmakers in the catalogue, sportsbooks and exchanges and prediction markets under one schema. One /v4/odds call shows you who is on the board. One /v4/historical-odds call, free on every plan, shows you when each of them arrived.

Scrape the sportsbooks OddsPapi
Games 2 to 7 Missing until each book posts them Listed as fixtures from day one, with Kalshi and Polymarket quotes days ahead
Books on Game 1 The sites you scrape 273 feeds on one call, deduped to 62 distinct price pairs in Step 2
When a book opened a game Only if your poller was running First snapshot per book on /v4/historical-odds
Closing margins Your own archive Last active price before first pitch, any played game
New game alerts Re-poll every page Poll one fixture ID, or take pushed updates over the WebSocket feed

When each book opened each LCS game

The table below covers both League Championship Series. Each cell shows the time of the book’s first moneyline price on that game, measured from the end of the previous game in the same series (the fixture’s trueEndTime). A negative number means the book listed the game before the previous one had finished. The right half shows each book’s closing margin: the overround on its last active moneyline pair before the scheduled first pitch.

Game Start (UTC) Pinnacle DraftKings FanDuel Kalshi Pinnacle close DraftKings close FanDuel close Kalshi close
NLCS G1 10-12 opener opener opener opener 1.97% 2.91% 2.86% 0.99%
NLCS G2 10-12 +0.6 h +1.1 h +1.1 h -74.7 h 2.04% 2.83% 3.35% 1.00%
NLCS G3 10-15 +1.1 h +1.5 h +1.3 h -102.0 h 2.07% 2.72% 2.86% 1.00%
NLCS G4 10-16 +6.4 h +1.6 h +1.4 h -152.8 h 2.08% 2.72% 2.62% 1.99%
ALCS G1 10-13 opener opener opener opener 2.08% 2.81% 2.97% 1.01%
ALCS G2 10-14 +0.4 h +2.1 h +0.8 h -46.9 h 2.04% 2.80% 2.91% 1.00%
ALCS G3 10-15 +0.2 h +1.4 h +1.3 h -70.6 h 1.93% 2.73% 2.79% 2.00%
ALCS G4 10-18 +30.1 h +31.5 h +30.4 h -39.3 h 1.95% 2.66% 2.86% 0.99%
ALCS G5 10-18 +1.4 h +1.7 h +1.8 h -19.6 h 1.99% 2.79% 2.87% 0.99%
ALCS G6 10-20 +0.6 h +1.8 h +0.6 h -0.2 h 2.04% 2.90% 3.37% 1.00%

ALCS Game 4 breaks the pattern for a known reason. Its first date, 17 October, shows up on the feed as a Cancelled fixture, and the books posted the new date 30 hours after Game 3. We left it out of the medians. Across the other 19 follow-on games of the postseason (1 to 20 October), the first sportsbook arrived 12 to 82 minutes after the last out, and Kalshi beat the first sportsbook to the game on 17 of 19, by a median 65 hours.

Three more things the table shows:

  • Pinnacle closed every playoff game between 1.82% and 2.24%. Its smaller-side stake limit at the close had a median of 37,500 across the 29 games, which is the depth you can lean on for a fair price.
  • DraftKings closed the first 18 playoff games at 4.49% to 4.76%, and the 11 games from 11 October onward at 2.66% to 2.91%. The step happened between Guardians v White Sox Game 4 on 9 October and Game 5 on 11 October. On Game 1 of the World Series, eleven hours out, DraftKings quoted 4.61% again, so read its close, not its open.
  • Kalshi closed 27 of 29 games at 0.98% to 1.02%. The other two read 1.99% and 2.00%.

The same “next game opens after the last one ends” pattern holds in other sports with a series or a short turnaround; our study of when NFL odds open measures the same clock across a full NFL season.

Step 1: List the series and see who is on each game

Sign up for a free key and export it as ODDSPAPI_API_KEY. MLB is tournamentId 109. Filter on it and /v4/fixtures returns the whole postseason window in one call. The feed also carries a Cancelled placeholder named “American League Champion v National League Champion”, and it marks a rained-out date the same way, so drop that status first.

import os, time, requests

API_KEY = os.environ["ODDSPAPI_API_KEY"]
BASE = "https://api.oddspapi.io/v4"

def get(path, **params):
    params["apiKey"] = API_KEY
    for _ in range(5):
        r = requests.get(f"{BASE}/{path}", params=params, timeout=120)
        if r.status_code == 429:
            time.sleep(r.json().get("error", {}).get("retryMs", 2000) / 1000 + 0.5)
            continue
        r.raise_for_status()
        return r.json()
    raise RuntimeError(f"{path} kept returning 429")

# MLB is tournamentId 109. `to` is a midnight-UTC instant, so go one day past the end.
fixtures = get("fixtures", tournamentId=109, **{"from": "2026-10-23", "to": "2026-11-03"})
series = [f for f in fixtures if f["statusName"] != "Cancelled"]

for f in sorted(series, key=lambda f: f["startTime"]):
    board = get("odds", fixtureId=f["fixtureId"])
    books = sorted(board.get("bookmakerOdds", {}))
    label = ", ".join(books) if len(books) <= 4 else f"{len(books)} books"
    print(f'{f["startTime"][:16]}  {f["participant1Name"]} v {f["participant2Name"]}  ->  {label}')
    time.sleep(1.0)

Output on 23 October 2026, 13:35 UTC:

2026-10-24T00:00  Milwaukee Brewers v Tampa Bay Rays  ->  273 books
2026-10-25T00:00  Milwaukee Brewers v Tampa Bay Rays  ->  fliff, kalshi, polymarket, polymarket.us
2026-10-27T00:00  Tampa Bay Rays v Milwaukee Brewers  ->  fliff, kalshi, polymarket, polymarket.us
2026-10-28T00:00  Tampa Bay Rays v Milwaukee Brewers  ->  kalshi, polymarket, polymarket.us
2026-10-29T00:00  Tampa Bay Rays v Milwaukee Brewers  ->  polymarket
2026-10-31T00:00  Milwaukee Brewers v Tampa Bay Rays  ->  
2026-11-01T00:00  Milwaukee Brewers v Tampa Bay Rays  ->  neds.com.au

Game 1 is a full board. Games 2 to 5 hold the prediction markets (fliff flickers on and off with suspended prices), Game 6 is empty, and Game 7 has one Australian book with its market switched off. Participant order follows the home team, so Games 3 to 5 read Rays v Brewers.

Step 2: Price Game 1 across every live book

The World Series moneyline is market 131, outcomes 131 (home) and 132 (away). Baseball also ships a nine-inning result with a tie leg under 133, and two first-five-innings markets; our first five innings guide covers those clocks, and the MLB odds API guide covers run lines and totals. Stay on 131 for the series winner of each game.

One parser rule matters on this board. On a 13:14 UTC read, 115 of the 206 books with a fresh, active Game 1 moneyline also ship marketActive: false on it, Kalshi, Novig, ProphetX, SX Bet and bet365 among them. If you filter on marketActive, you throw away the tightest prices on the board. Filter on the book-level suspended and bookmakerIsActive flags, the price-level active flag, and the age of the book’s newest changedAt.

import os, time, statistics, requests
from collections import defaultdict
from datetime import datetime, timezone

API_KEY = os.environ["ODDSPAPI_API_KEY"]
BASE = "https://api.oddspapi.io/v4"
GAME_1 = "id1300010975060814"        # Brewers v Rays, first pitch 2026-10-24 00:00 UTC

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

board = requests.get(f"{BASE}/odds", params={"apiKey": API_KEY, "fixtureId": GAME_1}, timeout=120).json()
now = datetime.now(timezone.utc)

quotes = {}
for slug, book in board["bookmakerOdds"].items():
    if "+" in slug or slug in ("demo", "singbet-b"):          # internal test feeds
        continue
    if book.get("suspended") or book.get("bookmakerIsActive") is False:
        continue
    markets = book.get("markets") or {}
    stamps = [ts(p["changedAt"]) for m in markets.values() for o in m["outcomes"].values()
              for p in o["players"].values() if p.get("changedAt")]
    if not stamps or (now - max(stamps)).total_seconds() > 3600:   # whole feed gone quiet
        continue
    legs = (markets.get("131") or {}).get("outcomes", {})
    try:
        home, away = legs["131"]["players"]["0"], legs["132"]["players"]["0"]
    except KeyError:
        continue
    # Read the price-level flag. marketActive is false on fresh, live prices here.
    if home.get("active") is False or away.get("active") is False:
        continue
    if 1 / home["price"] + 1 / away["price"] <= 1.0:                # one book never arbs itself
        continue
    quotes[slug] = (home["price"], away["price"])

med_h = statistics.median(q[0] for q in quotes.values())
med_a = statistics.median(q[1] for q in quotes.values())
quotes = {s: q for s, q in quotes.items()
          if abs(q[0] / med_h - 1) <= 0.10 and abs(q[1] / med_a - 1) <= 0.10}

groups = defaultdict(list)                                         # dedupe on the price pair
for slug, q in quotes.items():
    groups[q].append(slug)

def margin(q):
    return 100 * (1 / q[0] + 1 / q[1] - 1)

fair_home = statistics.median((1 / h) / (1 / h + 1 / a) for h, a in groups)
print(f"{len(quotes)} books, {len(groups)} distinct price pairs")
print(f"deduped consensus: Brewers {fair_home:.1%}  Rays {1 - fair_home:.1%}")
print("tightest pairs:")
for q in sorted(groups, key=margin)[:6]:
    print(f"  {q[0]:>6} / {q[1]:<6} {margin(q):5.2f}%  {', '.join(sorted(groups[q])[:4])}")
for slug in ("pinnacle", "fanduel", "draftkings", "bet365"):
    if slug in quotes:
        print(f"  {slug:<11} {quotes[slug][0]} / {quotes[slug][1]}  {margin(quotes[slug]):.2f}%")

Output on 23 October 2026, 13:35 UTC:

207 books, 62 distinct price pairs
deduped consensus: Brewers 63.0%  Rays 37.0%
tightest pairs:
   1.578 / 2.712   0.24%  sx.bet
   1.587 / 2.667   0.51%  novig.us, polymarket.us
   1.575 / 2.7     0.53%  prophetx
    1.58 / 2.68    0.60%  betfair-ex, fair999-ex, orbitx, piwi247-ex
   1.587 / 2.632   1.01%  kalshi, polymarket
    1.54 / 2.7     1.97%  duel
  pinnacle    1.549 / 2.66  2.15%
  fanduel     1.51 / 2.64  4.10%
  draftkings  1.505 / 2.62  4.61%
  bet365      1.48 / 2.63  5.59%

207 books collapse to 62 distinct price pairs, because skins share feeds: 27 slugs quote the same 1.50 / 2.60, and Pinnacle’s price appears on eight slugs. The deduped consensus puts the Brewers at 63.0%; Pinnacle de-vigs to 63.2%. Five exchanges and prediction markets quote the pair at 0.60% or less, so for a fair price on a World Series game you anchor on them and Pinnacle rather than on the retail books at 4% to 5.6%. Our consensus odds guide covers the de-vig methods in full.

Step 3: Measure when each book opened a played game

/v4/historical-odds returns every snapshot the feed recorded on a fixture, up to three bookmakers per call. Pass outcomeId=131 and the payload shrinks to the home moneyline only: 81 to 203 KB for Pinnacle, DraftKings and FanDuel together on these games. Kalshi goes in its own call; at 1.5 to 7.3 MB per outcome on these games it is still small enough for a loop. The fixture endpoint gives you trueEndTime, the recorded end of each game.

import os, time, requests
from datetime import datetime

API_KEY = os.environ["ODDSPAPI_API_KEY"]
BASE = "https://api.oddspapi.io/v4"
NLCS = ["id1300010975060874", "id1300010975060876",       # Dodgers v Brewers, played
        "id1300010975060878", "id1300010975060880"]        # 12-16 Oct 2026

def get(path, **params):
    params["apiKey"] = API_KEY
    for _ in range(5):
        r = requests.get(f"{BASE}/{path}", params=params, timeout=180)
        if r.status_code == 429:
            time.sleep(r.json().get("error", {}).get("retryMs", 2000) / 1000 + 0.5)
            continue
        r.raise_for_status()
        return r.json()
    raise RuntimeError(f"{path} kept returning 429")

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

def first_quote(fixture_id, books):
    # outcomeId=131 (home win) shrinks the payload from megabytes to kilobytes
    hist = get("historical-odds", fixtureId=fixture_id, bookmakers=books, outcomeId=131)
    time.sleep(4.6)
    out = {}
    for slug, book in hist["bookmakers"].items():
        snaps = book["markets"]["131"]["outcomes"]["131"]["players"]["0"]
        out[slug] = min(ts(s["createdAt"]) for s in snaps)
    return out

prev_end = None
for n, fid in enumerate(NLCS, start=1):
    meta = get("fixture", fixtureId=fid)
    time.sleep(1.0)
    opened = first_quote(fid, "pinnacle,draftkings,fanduel")
    opened.update(first_quote(fid, "kalshi"))
    row = []
    for slug in ("pinnacle", "draftkings", "fanduel", "kalshi"):
        if prev_end is None:
            row.append(f"{slug} {opened[slug]:%d %b %H:%M}")
        else:
            hours = (opened[slug] - prev_end).total_seconds() / 3600
            row.append(f"{slug} {hours:+.1f} h")
    print(f"Game {n}: " + " | ".join(row))
    prev_end = ts(meta["trueEndTime"])

Output on 23 October 2026 (the NLCS, Dodgers v Brewers, played 12 to 16 October):

Game 1: pinnacle 08 Oct 14:17 | draftkings 08 Oct 09:08 | fanduel 08 Oct 09:11 | kalshi 08 Oct 23:46
Game 2: pinnacle +0.6 h | draftkings +1.1 h | fanduel +1.1 h | kalshi -74.7 h
Game 3: pinnacle +1.1 h | draftkings +1.5 h | fanduel +1.3 h | kalshi -102.0 h
Game 4: pinnacle +6.4 h | draftkings +1.6 h | fanduel +1.4 h | kalshi -152.8 h

Swap in any series. The historical odds API guide explains the closing-line cut and the three-book cap, and the backtesting guide shows how to turn the same calls into a season of closes.

Step 4: Price the next game before the books post it

Until the sportsbooks arrive, the prediction markets are the line. The snippet below reads them off the next game, de-vigs each feed, and prints the stake behind each side. Run it in a loop after the last out and it returns the full board once 20 sportsbooks have posted.

import os, time, requests

API_KEY = os.environ["ODDSPAPI_API_KEY"]
BASE = "https://api.oddspapi.io/v4"
GAME_2 = "id1300010975060816"        # Brewers v Rays, first pitch 2026-10-25 00:00 UTC
PREDICTION = {"kalshi", "polymarket", "polymarket.us", "novig.us", "prophetx", "sx.bet"}

def read_board(fixture_id):
    r = requests.get(f"{BASE}/odds", params={"apiKey": API_KEY, "fixtureId": fixture_id}, timeout=120)
    r.raise_for_status()
    return r.json().get("bookmakerOdds", {})

def early_line(books):
    for slug in sorted(PREDICTION & books.keys()):
        legs = (books[slug].get("markets") or {}).get("131", {}).get("outcomes", {})
        if "131" not in legs or "132" not in legs:
            continue
        h, a = legs["131"]["players"]["0"], legs["132"]["players"]["0"]
        if h.get("active") is False or a.get("active") is False:
            continue
        fair = (1 / h["price"]) / (1 / h["price"] + 1 / a["price"])
        print(f"  {slug:<14} Brewers {h['price']:<6} Rays {a['price']:<6} "
              f"no-vig Brewers {fair:.1%}  depth ${h.get('limit') or 0:,.0f} / ${a.get('limit') or 0:,.0f}")

def wait_for_sportsbooks(fixture_id, min_books=20, every=600, checks=36):
    for _ in range(checks):
        books = read_board(fixture_id)
        sportsbooks = [s for s in books if s not in PREDICTION]
        print(f"{len(books)} feeds on the board, {len(sportsbooks)} sportsbooks")
        if len(sportsbooks) >= min_books:
            return books
        early_line(books)
        time.sleep(every)
    return None

wait_for_sportsbooks(GAME_2, checks=1, every=0)

Output on 23 October 2026, 13:35 UTC, for Game 2:

3 feeds on the board, 0 sportsbooks
  kalshi         Brewers 1.695  Rays 2.326  no-vig Brewers 57.8%  depth $793 / $344
  polymarket     Brewers 1.667  Rays 2.326  no-vig Brewers 58.3%  depth $272 / $86
  polymarket.us  Brewers 1.739  Rays 2.326  no-vig Brewers 57.2%  depth $145 / $212

The three feeds agree within 1.1 points on Game 2. Read the depth before you trust a number: a few hundred dollars per side sits behind these quotes two days out, against the 20,491 Pinnacle limit on the Brewers in Game 1 at the same moment. Quotes on the later games can read far off a fair pair, because those games may never be played: Polymarket’s Game 5 pair read 1.25 / 1.282, which implies 158%. Reject any two-way quote whose implied probabilities do not sum to about 100%. Our Polymarket and Kalshi API guide covers both venues’ market formats.

Polling every ten minutes works, but the WebSocket feed pushes each new book the moment it posts, inside that 41-minute window.

World Series odds API FAQ

Is there a free World Series odds API?

Yes. OddsPapi’s free plan returns live moneylines, run lines and totals on every World Series game from 350+ bookmakers, prediction markets included, plus the full price history of every played postseason game. MLB is tournamentId 109; each game is its own fixture.

When do sportsbooks post odds for the next World Series game?

After the previous game ends. On 19 follow-on games of the 2026 MLB postseason, the first of Pinnacle, DraftKings and FanDuel posted the next game a median 41 minutes after the last out, and within 82 minutes every time. Kalshi listed 16 of those games before the previous game had finished.

Which book has the tightest World Series moneyline?

On Game 1, eleven hours out, the exchanges and prediction markets: SX Bet, Novig, Polymarket US, ProphetX and Betfair quoted the pair at 0.24% to 0.60%. Pinnacle quoted 2.15% with a 20,491 limit on the favourite. Pinnacle closed every 2026 playoff game between 1.82% and 2.24%.

Can I get the closing odds of past World Series and playoff games?

Yes. /v4/historical-odds returns every recorded snapshot for up to three bookmakers per call. Take the last price with active: true before the scheduled startTime. Pass outcomeId to cut the payload to one outcome.

Why does a World Series board show marketActive false on live prices?

Some feeds ship the flag as false on fresh, active quotes; on Game 1 that was 115 of 206 books with a live moneyline. Read the price-level active flag and each book’s newest changedAt instead.

Read the whole series from one key

Every call above runs on the free key: 350+ bookmakers under one schema, free historical odds on every played game, and a WebSocket feed for the hour after the last out. Get a free API key and run Step 1 on tonight’s game.