MLB Odds API: Price the First Five Innings Across 168 Books

MLB Odds API first five innings market IDs
How To Guides September 25, 2026

You pull an MLB fixture from the OddsPapi /odds endpoint, filter on market 131, and you have the moneyline from 256 bookmakers. That price settles on the final score, extra innings included. The five-inning price that sharp bettors actually want sits on two other market IDs, and neither one of them is 131.

On Cincinnati Reds v Los Angeles Dodgers (id1300010963303281), measured 17 September 2026 at 13:38 UTC, the first-five board carried 168 bookmakers. Forty of them posted it only as market 131256. Another forty posted it only as market 131258. Query one ID and you throw away a quarter of the board, including whichever book happens to be pricing it tightest.

This guide shows you how to build the merged first-five board in Python, rank it, and prove which clock each market settles on using the free /settlements endpoint.

Why the Usual Approach Misses Half the Board

Most odds feeds hand you a flat list of markets with a label on each one and leave the rest to you. Three habits break on baseball.

Hardcoding a market ID. Baseball ships 794 market definitions in the OddsPapi catalogue. The same bet appears under more than one ID, and the second ID is never the one you found first.

Matching on the market name. The catalogue names are honest here: Winner First To Fifth Inning and First To Fifth Inning Result both say what they cover. A substring match on “First To Fifth Inning” catches both, and it also catches the other 148 definitions that share the phrase, most of them handicap and total rungs. It still tells you nothing about which of the two carries a tie leg.

Trusting hasOdds. That flag says a board exists. It says nothing about which markets the books opened. Probe /odds before you commit.

The field that actually separates the clocks is period. Every baseball market definition carries one, and the values are literal inning ranges.

Approach What You Get Cost
Scrape a single sportsbook One book’s menu, one shape, blocked within a week Free, then broken
Generic odds API Full-game moneylines from ~20 books, no inning splits $99+/month
Enterprise feed Everything, after a sales call and an annual contract Quote on request
OddsPapi free tier 350+ bookmakers, every inning period, free historical odds, WebSocket push $0

Step 1: Read the Baseball Market Map

Authentication is a query parameter. There is no header and no OAuth dance.

import os, requests, collections

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

markets = requests.get(f"{BASE}/markets", params={"apiKey": API_KEY}).json()
baseball = [m for m in markets if m["sportId"] == BASEBALL]

by_period = collections.Counter(m["period"] for m in baseball)
print(f"{len(baseball)} baseball market definitions")
for period, n in by_period.most_common():
    print(f"  {str(period):<24} {n}")

Run it and the sport’s whole structure falls out:

794 baseball market definitions
  result                   258
  p1+p2+p3+p4+p5           150
  p6+p7+p8+p9+overtime     150
  p1                       26
  p2                       26
  ...
  fulltime                 1
  None                     1

/markets ignores the sportId parameter and returns all 33,115 definitions across every sport, so filter client-side. That is the same behaviour documented for basketball and it has not changed.

Four of those definitions answer “who wins”. Print them:

for m in baseball:
    if m["marketType"] in ("moneyline", "1x2") and m["period"] in (
            "result", "fulltime", "p1+p2+p3+p4+p5"):
        legs = "/".join(o["outcomeName"] for o in m["outcomes"])
        print(f"{m['marketId']:>7}  {m['marketName']:<42} "
              f"period={m['period']:<18} legs={legs}")
    131  Winner (incl. extra innings)               period=result             legs=2/1
    133  Regular Time Result                        period=fulltime           legs=X/2/1
 131256  Winner First To Fifth Inning               period=p1+p2+p3+p4+p5     legs=2/1
 131258  First To Fifth Inning Result               period=p1+p2+p3+p4+p5     legs=2/1/X

Look at the leg order. Market 131 lists the away leg first, 133 leads with the tie, 131258 puts the tie last. Match on outcomeName and never on array position. Baseball also reuses market IDs as outcome IDs, so market 131256 owns outcome 131256. Keep the pair as a tuple rather than keying a dict on a bare integer.

Step 2: Merge the Two First-Five IDs Into One Board

The resolver reads both IDs and returns one board keyed by bookmaker slug. Four guards run on every read: the book-level suspended flag, the market-level marketActive flag, the price-level active flag, and a leg count.

F5_TWO_WAY = 131256    # Winner First To Fifth Inning
F5_THREE_WAY = 131258  # First To Fifth Inning Result

LEGS = {
    F5_TWO_WAY:   {"131256": "1", "131257": "2"},
    F5_THREE_WAY: {"131258": "1", "131259": "X", "131260": "2"},
}

def read_market(book, market_id):
    """Return {leg_name: price} for one book, or None if a guard fails."""
    market = book["markets"].get(str(market_id))
    if market is None or not market.get("marketActive"):
        return None
    if book.get("suspended"):
        return None
    legs = {}
    for outcome_id, outcome in market["outcomes"].items():
        price = outcome["players"].get("0")
        if price is None or not price["active"]:
            return None
        legs[LEGS[market_id][outcome_id]] = price["price"]
    if len(legs) != len(LEGS[market_id]):
        return None
    return legs

def f5_board(fixture_id):
    payload = requests.get(f"{BASE}/odds",
                           params={"apiKey": API_KEY,
                                   "fixtureId": fixture_id}).json()
    board = {}
    for slug, book in payload.get("bookmakerOdds", {}).items():
        two = read_market(book, F5_TWO_WAY)
        three = read_market(book, F5_THREE_WAY)
        if two:
            board.setdefault(slug, {})["two_way"] = two
        if three:
            board.setdefault(slug, {})["three_way"] = three
    return board

board = f5_board("id1300010963303281")
two_only = [s for s, v in board.items() if "two_way" in v and "three_way" not in v]
three_only = [s for s, v in board.items() if "three_way" in v and "two_way" not in v]
both = [s for s, v in board.items() if len(v) == 2]
print(f"{len(board)} books on the first-five board")
print(f"  two-way only   {len(two_only)}")
print(f"  three-way only {len(three_only)}")
print(f"  both           {len(both)}")
168 books on the first-five board
  two-way only   40
  three-way only 40
  both           88

The leg count is not a formality. On New York Mets v Philadelphia Phillies the same afternoon, betano.cz, codere.es, overtime.io and pokerstars.es all posted market 131258 with the tie leg missing. Two legs where three belong sum to a smaller overround, so an unguarded margin ranking floats the incomplete books to the top.

The Split Is Not Random

The two market IDs draw different bookmaker populations, and the split runs along the kind of operator rather than the region.

Market ID Shape Who sits here
131256 Two legs, home and away draftkings, novig.us, prophetx, sx.bet, circasports, sbobet, coral, ladbrokes, hardrockbet, betus
131258 Three legs, tie priced polymarket, polymarket.us, kalshi, pokerstars, winamax.de, dafabet, midnite, duel, bcgame, cloudbet
Both 88 books fanduel, betmgm, betano and its skins, 20bet, bet99

US retail and the P2P exchanges sit on the two-way ID. The prediction markets and the crypto casinos sit on the three-way. Both pinnacle and betfair-ex sat on that board with a full-game price and carried neither five-inning market, so the sharp anchor you would normally reach for has nothing to say about the first five. Build your fair value from the deduplicated board instead.

Three responses tell you why a slug is missing, and they are easy to confuse. Pass bookmakers=pinnacle alone and you get HTTP 200 with the slug in the payload and no market 131256 inside it, which means the book opened no such market. A slug outside the catalogue returns 400 INVALID_PARAMETER and the error body lists every valid slug. A slug your plan cannot read returns 403 RESTRICTED_ACCESS and names it. Check the status code before you conclude anything about coverage.

Step 3: Rank the Merged Board

Once both IDs feed one board you can rank all 168 books on margin. Use the two-way quote where a book has one, the three-way otherwise.

import statistics, collections

def margin(legs):
    return sum(1 / price for price in legs.values()) - 1

ranked = []
for slug, quotes in board.items():
    legs = quotes.get("two_way") or quotes["three_way"]
    shape = "2-way" if "two_way" in quotes else "3-way"
    ranked.append((margin(legs), slug, shape))
ranked.sort()

print(f"{len(ranked)} books, median margin "
      f"{statistics.median(m for m, _, _ in ranked) * 100:.2f}%")
for m, slug, shape in ranked[:8]:
    print(f"  {slug:<18} {shape}  {m * 100:5.2f}%")

tuples = collections.Counter(
    tuple(sorted((q.get("two_way") or q["three_way"]).items()))
    for q in board.values())
print(f"dedupe: {len(board)} quotes to {len(tuples)} distinct prices "
      f"({100 * (1 - len(tuples) / len(board)):.1f}% collapse)")
168 books, median margin 6.95%
  novig.us           2-way   0.50%
  polymarket.us      3-way   1.00%
  kalshi             3-way   2.00%
  betus              2-way   3.62%
  betwar             2-way   3.75%
  circasports        2-way   3.80%
  prophetx           2-way   4.04%
  apuestatotal       2-way   4.19%
dedupe: 168 quotes to 61 distinct prices (63.7% collapse)

The first and second place finishers sit on different market IDs. novig.us leads at 0.50% on the two-way; polymarket.us leads the three-way at 1.00%. Read either ID alone and the book at the top of your leaderboard is whichever one your query happened to include.

The dedupe number matters as much as the count. Those 168 quotes collapse to 61 independent prices, so 63.7% of the board is copying somebody. The cloneOf field flags 117 of the 360 catalogue slugs and it does not predict this: distinct brands under separate licences quote byte-identical prices with cloneOf set to null. Deduplicate on the price tuple before you average anything, the same way you would for a consensus line.

Pick the De-Vig Method Before You Compare IDs

Eighty-eight books on that fixture posted both first-five markets. Strip the tie from the three-way, renormalise, and their two views should agree. Whether they do depends entirely on how you remove the margin.

def power_devig(legs):
    """Solve sum(raw ** n) == 1 by bisection."""
    raw = [1 / price for price in legs.values()]
    low, high = 0.2, 4.0
    for _ in range(80):
        mid = (low + high) / 2
        if sum(r ** mid for r in raw) > 1:
            low = mid
        else:
            high = mid
    n = (low + high) / 2
    return {leg: (1 / price) ** n for leg, price in legs.items()}

gaps = []
for slug, quotes in board.items():
    if len(quotes) != 2:
        continue
    two = power_devig(quotes["two_way"])
    three = power_devig(quotes["three_way"])
    conditional = three["1"] / (three["1"] + three["2"])
    gaps.append(abs(two["1"] - conditional) * 100)
print(f"{len(gaps)} books post both IDs; median self-disagreement "
      f"{statistics.median(gaps):.2f} points")
88 books post both IDs; median self-disagreement 0.16 points

Sixteen hundredths of a point. The books hold the same view on both IDs, so the merged board is safe to treat as one market. The Mets fixture gives the same answer: 85 books posted both first-five markets and the median gap ran 0.20 points under proportional de-vig and 0.40 under the power method, with 44 books on the full-game pair at 0.43 and 0.53.

Where the method does change your number is the tie leg itself. On those 44 nine-inning three-ways, proportional de-vig puts the tie at a median 10.44% while the power method puts it at 9.19%. Proportional de-vig divides every implied probability by the same overround, which hands too much of the margin to the long leg, and a tie leg is always the long leg.

The realised rate settles the argument. Pulling /v4/scores for all 216 MLB fixtures played between 1 and 16 September 2026 and comparing fulltime against result, 21 of them went past nine innings. That is 9.72%. The power method’s 9.19% sits half a point away from the season’s own number; proportional de-vig’s 10.44% sits three quarters of a point the other side. Our no-vig guide works through all three methods.

Step 4: Prove Which Clock Each Market Settles On

Everything above is pricing. The settlement grid is where you find out what you actually bought, and /v4/settlements is on the free tier.

San Francisco Giants v St. Louis Cardinals (id1300010963301511, first pitch 8 September 2026 at 00:28 UTC) finished 3-3 after nine innings and 5-4 after extras. That single fixture separates two markets that look identical in a market list.

FULL_GAME = 131    # Winner (incl. extra innings), 2-way
REGULATION = 133   # Regular Time Result, 3-way with a tie leg
NAMES = {"131": "home", "132": "away",
         "133": "home", "134": "tie", "135": "away"}

def grade(fixture_id):
    settle = requests.get(f"{BASE}/settlements",
                          params={"apiKey": API_KEY,
                                  "fixtureId": fixture_id}).json()
    out = {}
    for market_id in (FULL_GAME, REGULATION):
        market = settle["markets"].get(str(market_id))
        out[market_id] = None if market is None else {
            NAMES[oid]: leg["players"]["0"]["result"]
            for oid, leg in market["outcomes"].items()}
    return out

periods = requests.get(f"{BASE}/scores",
                       params={"apiKey": API_KEY,
                               "fixtureId": "id1300010963301511"}
                       ).json()["scores"]["periods"]
print("after 9:", periods["fulltime"], "final:", periods["result"])
print(grade("id1300010963301511"))
after 9: 3-3   final: 5-4
{131: {'home': 'WIN', 'away': 'LOSE'},
 133: {'home': 'LOSE', 'tie': 'WIN', 'away': 'LOSE'}}

One fixture, two markets, opposite results on the home leg. Market 131 paid the home side because the home side won 5-4. Market 133 paid the tie leg because the score after nine innings was level. A grader that merges the two IDs on the grounds that both say “who wins” pays the wrong side on 21 of the 216 MLB fixtures played in the first half of September 2026.

The /scores response is the cheap detector. periods["fulltime"] is the score after nine, periods["result"] is the final. Diff them and you know whether the fixture went to extras without parsing a single price. The settlement grid on that fixture covered 771 markets, so the same call grades the first-five pair and the inning markets in one pass.

Step 5: Pull the First-Five History

/historical-odds is free and it keeps the inning splits. It takes up to three bookmaker slugs per call, and one unreadable slug fails the whole batch, so retry a failure one slug at a time.

def price_moves(snapshots, cutoff):
    """Collapse consecutive identical prices. A raw snapshot count
    measures your poller, not the book."""
    live = [s for s in snapshots if s["active"] and s["createdAt"] < cutoff]
    moves, last = 0, None
    for snap in live:
        if snap["price"] != last:
            moves += 1
            last = snap["price"]
    return live, moves

history = requests.get(f"{BASE}/historical-odds",
                       params={"apiKey": API_KEY,
                               "fixtureId": "id1300010963301511",
                               "bookmakers": "draftkings,fanduel,betmgm"}).json()

CUTOFF = "2026-09-08T00:28:12"
for slug, book in history["bookmakers"].items():
    for market_id, outcome_id in ((131, 131), (131256, 131256), (131258, 131258)):
        market = book["markets"].get(str(market_id))
        if market is None:
            print(f"{slug:<12} market {market_id:<7} absent")
            continue
        snaps = market["outcomes"][str(outcome_id)]["players"]["0"]
        live, moves = price_moves(snaps, CUTOFF)
        print(f"{slug:<12} market {market_id:<7} {len(snaps):>5} snapshots "
              f"{len(live):>4} pre-match active {moves:>3} moves "
              f"{live[0]['price']} to {live[-1]['price']}")
Book Market Pre-match active snapshots Price moves Open to close
draftkings 131 full game 100 43 1.763 to 1.498
draftkings 131256 first five 97 50 1.69 to 1.317
fanduel 131 full game 257 36 1.74 to 1.47
fanduel 131256 first five 276 54 1.66 to 1.29
betmgm 131 full game 88 60 1.72 to 1.48
betmgm 131258 first five, three-way 120 81 1.95 to 1.48
draftkings 131258 first five, three-way absent, this book never posts the three-way shape

On DraftKings and FanDuel the five-inning line repriced more often than the nine-inning line on the same fixture, 50 moves against 43 and 54 against 36. The starting pitcher drives the first-five number and the bullpen does not, so the market reacts to lineup news that the full-game price absorbs more slowly. That is the signal worth backtesting.

One warning on the same pull: draftkings carried no history at all for market 131258, because it never posts the three-way shape. A single-book first-five backtest therefore learns whichever ID that book happens to favour. Pull at least two books from opposite sides of the split. The whole three-book call returned 82 MB, so page it rather than holding it in memory, and remember that raw snapshot counts measure your poller: DraftKings logged 419,909 snapshots across 214 markets on that one fixture while FanDuel logged 342,719 across 122.

What the Rest of the Baseball Catalogue Holds

The same period field opens the rest of the sport. Across eight MLB boards measured on 17 September 2026 between 13:15 and 13:25 UTC, each fixture carried 265 to 269 bookmakers, 456 to 497 distinct market IDs, and 29.7 to 33.1 MB of unfiltered JSON.

Period value Definitions What it settles on
result 258 Final score, extra innings included
fulltime 1 Score after nine innings (market 133)
p1+p2+p3+p4+p5 150 First five innings
p6+p7+p8+p9+overtime 150 Sixth inning onward
p1 through p9 26 each One inning

Board depth by market on those eight fixtures, after the four guards:

Market Books quoting Median margin
131 Winner, incl. extra innings 254 to 260 5.09% to 5.34%
131256 Winner first five 124 to 130 6.25% to 6.94%
131258 First five result 126 to 127 7.79% to 8.81%
133 Regular time result 43 to 45 9.40% to 10.01%

Margin widens as the clock narrows. The full-game moneyline runs a little over 5%, the five-inning two-way about 6.5%, the nine-inning three-way close to 10%. Price a first-five bet against the full-game consensus and you are comparing a 5% market with a 7% one, which is exactly the trap line shopping across books is meant to close.

The individual-inning markets carry their own guard. Market 13100, the first-inning three-way, reached 174 books on the Mets fixture, and 31 of those failed the three-active-legs check. The same len(legs) guard that protects the first-five board protects this one.

Frequently Asked Questions

What is the market ID for MLB first five innings odds?

Two of them. Market 131256 (Winner First To Fifth Inning) is the two-leg version, and market 131258 (First To Fifth Inning Result) adds a tie leg. Both carry period of p1+p2+p3+p4+p5. Read both and merge on the bookmaker slug, which took the board from about 125 books to 168 on the fixture measured for this guide.

Does an MLB moneyline from the OddsPapi API include extra innings?

Market 131 does. Its period is result and it settles on the final score. Market 133 settles on the score after nine innings and pays its tie leg when the game goes to extras. On fixture id1300010963301511, which finished 3-3 after nine and 5-4 after extras, /v4/settlements graded the home leg of 131 as WIN and the tie leg of 133 as WIN.

How many bookmakers price MLB on the OddsPapi API?

Across eight MLB fixtures measured on 17 September 2026, each board carried 265 to 269 bookmakers and 456 to 497 distinct market IDs. After deduplicating on the price tuple, the 168-book first-five board resolved to 61 independent prices, so treat the count and the independent-price count as two separate numbers.

Which bookmaker prices MLB first five innings tightest?

On Reds v Dodgers at 13:38 UTC on 17 September 2026, novig.us led the two-way at a 0.50% margin and polymarket.us led the three-way at 1.00%, with kalshi third at 2.00%. They sit on different market IDs, so a leaderboard built from one ID returns a different winner than a leaderboard built from both.

Is historical MLB odds data free on OddsPapi?

Yes. /v4/historical-odds is on the free tier and it keeps the inning splits, including the first-five markets. It accepts up to three bookmaker slugs per call. One three-book pull on a single fixture returned 82 MB and 419,909 snapshots for DraftKings alone, so collapse consecutive identical prices into price moves before you count anything.

How do I tell whether an MLB fixture went to extra innings?

Call /v4/scores and compare two keys. periods["fulltime"] holds the score after nine innings and periods["result"] holds the final score. When they differ, the fixture went past regulation. The response also carries p1 through p9 as individual innings.

Get the Board

The free tier covers 350+ bookmakers, every baseball period from a single inning to the final score, and historical odds with no add-on. If you need the board pushed rather than polled, the same coverage streams over WebSocket.

Stop guessing which market ID your book uses. Grab a free API key and read both.

Related reading: MLB run lines, totals and moneylines, MLB player props in Python, Japanese baseball odds, and how many bookmakers a backtest actually needs.