NBA Second Half Odds API: 90 Lines Carry Two Settlement Rules

NBA Second Half Odds API featured image
How To Guides September 30, 2026

An NBA second-half total of 104.5 is two different bets. One settles on the points scored in the third and fourth quarters. The other counts overtime as well. Both carry the line number 104.5, both read as a totals market, and different bookmakers quote different ones.

The OddsPapi catalogue separates them with a field called period. Read it and the two bets separate. Skip it and a cross-book comparison lines up a regulation bet against an overtime bet and calls the gap an edge. Every number below comes from a call made on 21 September 2026.

Basketball Runs on Ten Clocks

One call to /v4/markets returns 33,115 definitions across every sport. Filter on sportId: 11 and 4,902 of them are basketball. Group those by period.

period Settles on Definitions What lives there
result the final score, overtime included 1,205 the two-way Winner, 401 totals, 321 spreads, every player prop
p3+p4+overtime the second half plus overtime 781 180 totals, 201 spreads, 400 team totals
p1+p2 the first half 691 90 totals, 201 spreads, 400 team totals
p1 the first quarter 609 totals, spreads, team totals, first-quarter player props
p4+overtime the fourth quarter plus overtime 521 100 totals, 141 spreads, 280 team totals
p2 the second quarter 473 totals, spreads, team totals
p3 the third quarter 473 totals, spreads, team totals
p3+p4 the second half, 48 minutes only 90 totals and nothing else
p4 the fourth quarter, 48 minutes only 52 50 totals, a three-way result, a two-way winner
fulltime the score after 48 minutes 1 market 113, Regular Time Result, with a draw leg
null unset 6 First Half Result, Second Half Result and four others

Two rows in that table shadow two other rows. p3+p4 is the second half without overtime and p3+p4+overtime is the second half with it. p4 and p4+overtime do the same job for the fourth quarter. The first half needs no twin, because overtime cannot reach it.

Every Regulation Line Number Has an Overtime Twin

Pull the handicap values off both ladders and the collision is total.

Ladder Rungs Span Step Line numbers shared with the twin
Over Under Second Half 90 40.5 to 129.5 1.0 90 of 90
Over Under Second Half (incl. overtime) 180 40.5 to 130.0 0.5 90 of 180
Over Under Fourth Quarter 50 20.5 to 69.5 1.0 50 of 50
Over Under Fourth Quarter (incl. overtime) 100 20.5 to 70.0 0.5 50 of 100

The regulation ladders are half-point rungs in steps of a full point. The overtime ladders take the same half-point rungs and add the whole numbers between them. Sort a merged board by line number and the two clocks interleave without a single duplicate to warn you.

The market name does carry the clock, in the suffix (incl. overtime). That suffix is the only thing separating the names, so any normaliser that strips parentheses, lowercases and trims will fuse the two ladders into one. Match on period, which is a short enum, rather than on a string you have to clean.

Old Way vs OddsPapi

Job Scraping or a single-book feed OddsPapi
Tell a regulation half from an overtime half Parse the market’s display name and hope Read period on /v4/markets
List every second-half line a book offers Walk one sportsbook’s markup Filter 4,902 definitions on two enum values
Compare the 104.5 across books Match on the number and merge two bets Key on (period, handicap)
Check what a closed market paid No history, so no check /historical-odds on the free tier
Grade it after the buzzer Scrape a scoreboard /v4/scores returns every quarter as its own key
Price Proxies, breakage, legal risk Free tier, 250 requests a month, no card

Step 1: Print the Clock Map

Authentication is a query parameter. The key rides on ?apiKey= and never in a header.

import os, requests, collections

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

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

clocks = collections.Counter(str(m["period"]) for m in basketball)
for period, count in clocks.most_common():
    print(f"{period:18} {count:>5} market definitions")

print(f"\n{len(basketball)} basketball market definitions in all")
result              1205 market definitions
p3+p4+overtime       781 market definitions
p1+p2                691 market definitions
p1                   609 market definitions
p4+overtime          521 market definitions
p2                   473 market definitions
p3                   473 market definitions
p3+p4                 90 market definitions
p4                    52 market definitions
None                   6 market definitions
fulltime               1 market definitions

4902 basketball market definitions in all

/v4/markets ignores a sportId parameter and hands back all 33,115 definitions, so filter on the client. Six of the basketball definitions carry period: None, among them Second Half Result and First Half Result. A dictionary keyed straight on period will trip over those six, so coerce the value before you use it as a key.

Step 2: Find the Twin Ladders

Group the definitions by marketName, then look for any name that also exists with the (incl. overtime) suffix.

ladders = collections.defaultdict(set)
for m in basketball:
    ladders[m["marketName"]].add(m["handicap"])

SUFFIX = " (incl. overtime)"
for name in sorted(ladders):
    twin = name + SUFFIX
    if twin not in ladders:
        continue
    regulation, overtime = ladders[name], ladders[twin]
    print(f"{name}")
    print(f"  regulation clock : {len(regulation):>3} lines, {min(regulation)} to {max(regulation)}")
    print(f"  overtime clock   : {len(overtime):>3} lines, {min(overtime)} to {max(overtime)}")
    print(f"  line numbers on both: {len(regulation & overtime)}\n")
Over Under Fourth Quarter
  regulation clock :  50 lines, 20.5 to 69.5
  overtime clock   : 100 lines, 20.5 to 70.0
  line numbers on both: 50

Over Under Second Half
  regulation clock :  90 lines, 40.5 to 129.5
  overtime clock   : 180 lines, 40.5 to 130.0
  line numbers on both: 90

Two ladders, 140 regulation line numbers, and all 140 appear again on the overtime clock. Spreads and team totals dodge the problem by existing on the overtime clock alone: p3+p4 carries 90 totals and nothing else, and p4 carries 50 totals plus a three-way result and a two-way winner. Ask for a regulation second-half spread and no book has one to sell you.

Step 3: Split a Live Board by Clock

Detroit Pistons v Boston Celtics opens the 2026-27 season on 20 October. One unfiltered /v4/odds call on it returned 9.23 MB at 13:09 UTC on 21 September, 29 days out.

FIXTURE = "id1100013273682354"   # Detroit Pistons v Boston Celtics, 20 Oct 2026

clock = {str(m["marketId"]): m["period"] for m in basketball}

board = requests.get(f"{BASE_URL}/odds",
                     params={"apiKey": API_KEY, "fixtureId": FIXTURE}).json()

ids = collections.defaultdict(set)
books = collections.defaultdict(set)
for slug, book in board["bookmakerOdds"].items():
    for market_id in book.get("markets", {}):
        period = clock.get(market_id, "UNKNOWN") or "null"   # 6 definitions ship period None
        ids[period].add(market_id)
        books[period].add(slug)

print(f"{len(board['bookmakerOdds'])} bookmakers on the fixture\n")
for period in sorted(ids, key=lambda p: -len(ids[p])):
    print(f"{period:14} {len(ids[period]):>4} market IDs  {len(books[period]):>4} bookmakers")
221 bookmakers on the fixture

result          526 market IDs   220 bookmakers
p1+p2             4 market IDs     4 bookmakers
fulltime          1 market IDs    65 bookmakers

Twenty-nine days out, four market IDs sit on a half or quarter clock. The result clock holds 526 of the 531 priced IDs and 220 of the 221 books. Market 113, Regular Time Result, draws 65 books on its own: that is the three-way bet on the 48-minute score, draw leg included, and it settles differently from the two-way Winner that 220 books quote on the same fixture. Both markets read the same scoreline and they pay out under different rules.

The quarter and half ladders fill in during the days before tip-off, which is where the trap sits. A resolver written against a T-29d board never meets the twin clocks, then meets both of them an hour before the first jump ball. Build the branch now and test it on a fixture that has already finished.

Step 4: Read the Twins on a Played Fixture

A live board outside the market-open window reports a false zero, so test coverage on a game that has been played. San Antonio Spurs v New York Knicks, Game 5 of the 2026 Finals, tipped at 00:30 UTC on 14 June. /historical-odds keeps the full menu after the buzzer and takes at most three bookmaker slugs a call.

FIXTURE = "id1100013270505012"   # San Antonio v New York, tipped 14 Jun 2026 00:30 UTC
TIP_OFF = "2026-06-14T00:30:00.000Z"
NAMES = ("Over Under Second Half", "Over Under Second Half (incl. overtime)")

defs = {str(m["marketId"]): m for m in basketball}

history = requests.get(f"{BASE_URL}/historical-odds",
                       params={"apiKey": API_KEY, "fixtureId": FIXTURE,
                               "bookmakers": "caesars,betrivers,draftkings"}).json()

def closing(snapshots):
    live = [s for s in snapshots if s["active"] and s["createdAt"] < TIP_OFF]
    return live[-1]["price"] if live else None

rows = collections.defaultdict(dict)
for slug, book in history["bookmakers"].items():
    for market_id, market in book.get("markets", {}).items():
        spec = defs.get(market_id)
        if not spec or spec["marketName"] not in NAMES:
            continue
        labels = {str(o["outcomeId"]): o["outcomeName"] for o in spec["outcomes"]}
        quote = {}
        for outcome_id, outcome in market.get("outcomes", {}).items():
            price = closing(outcome["players"]["0"])   # a LIST on this endpoint
            if price:
                quote[labels[outcome_id]] = price
        if len(quote) == 2:
            rows[spec["handicap"]][slug] = (spec["period"], quote)

for line in sorted(rows):
    for slug, (period, quote) in sorted(rows[line].items()):
        print(f"{line:>6}  {slug:<11} {period:<15} Over {quote['Over']:<6} Under {quote['Under']}")
 103.5  caesars     p3+p4           Over 1.833  Under 1.909
 104.5  betrivers   p3+p4+overtime  Over 1.84   Under 1.92
 104.5  caesars     p3+p4           Over 1.909  Under 1.833
 105.5  betrivers   p3+p4+overtime  Over 1.92   Under 1.84
 105.5  caesars     p3+p4           Over 1.909  Under 1.833
 106.5  betrivers   p3+p4+overtime  Over 1.93   Under 1.83

Caesars closed 104.5 on the regulation clock at 1.909 and BetRivers closed 104.5 on the overtime clock at 1.84. Merge on the line number and Caesars reads as a 3.8% better price on the Over. The two quotes are not competing, because they settle on different scores. Caesars ran 6.94% overround across all three of its rungs and BetRivers ran 6.43%, so the margin gap does not explain the price gap either.

The split runs deeper than one line. Across the eleven books read on that fixture, DraftKings priced the second half on both clocks, 25 market IDs on p3+p4 and 24 on p3+p4+overtime. Pinnacle priced 17 second-half IDs and 27 fourth-quarter IDs, every one of them on the overtime clock and none on regulation. Caesars went the other way on the second half. Rank a second-half board across those three books without branching and you rank three different bets.

Step 5: Grade Both Clocks

/v4/scores returns each quarter under its own key, so you can build either settlement figure yourself.

periods = requests.get(f"{BASE_URL}/scores",
                       params={"apiKey": API_KEY, "fixtureId": FIXTURE}
                       ).json()["scores"]["periods"]

def total(key):
    p = periods[key]
    return p["participant1Score"] + p["participant2Score"]

q3_q4 = total("p3") + total("p4")                          # p3+p4
second_half_with_ot = total("result") - total("p1") - total("p2")   # p3+p4+overtime

print(f"quarters       {total('p1')} / {total('p2')} / {total('p3')} / {total('p4')}")
print(f"regulation     {total('fulltime')}")
print(f"final          {total('result')}")
print(f"p3+p4          {q3_q4}")
print(f"p3+p4+overtime {second_half_with_ot}")

for line in (103.5, 104.5, 105.5, 106.5):
    reg = "Over" if q3_q4 > line else "Under"
    ot = "Over" if second_half_with_ot > line else "Under"
    print(f"{line:>6}  p3+p4 {reg:<5}  p3+p4+overtime {ot}")
quarters       36 / 43 / 58 / 47
regulation     184
final          184
p3+p4          105
p3+p4+overtime 105
 103.5  p3+p4 Over   p3+p4+overtime Over
 104.5  p3+p4 Over   p3+p4+overtime Over
 105.5  p3+p4 Under  p3+p4+overtime Under
 106.5  p3+p4 Under  p3+p4+overtime Under

Game 5 finished in regulation, so fulltime and result both read 184 and the two clocks graded the same way on all four lines. That is the ordinary case, and it is what makes the bug quiet. The clocks only separate when a game goes past 48 minutes, and a resolver that has never met one of those games looks correct for a whole season of backtests.

The detector is one comparison. If fulltime and result differ, the game went to overtime and every p3+p4 grade parts company with its p3+p4+overtime twin. Add it to your settlement loop and the exceptions surface on their own.

The Rule

Key every basketball market on the pair (period, handicap), never on the name and never on the line number. That one change fixes four separate comparisons:

  • Second-half totals, where 90 line numbers sit on two clocks.
  • Fourth-quarter totals, where 50 do.
  • The full-game winner, where market 111 settles after overtime and market 113 settles at 48 minutes with a draw leg.
  • Any consensus or best-price scan that pools books, because Pinnacle and Caesars sit on opposite clocks for the same headline bet.

The same field carries the same weight in ice hockey, where 118 markets ship in both flavours, and in baseball, where the result settles under four IDs across three clocks. Basketball is the version where the two ladders overlap on every rung.

Get the Board

One /v4/odds call returns every market a fixture carries, both clocks included, from a catalogue of 350+ bookmakers. Historical snapshots come on the same key at no extra cost, which is what makes a played-fixture test cheap enough to run before you trust a resolver. WebSocket delivery and the native market structures, Asian handicaps and player props among them, sit behind the same API.

Start on the free tier: 250 requests a month, every sport, every bookmaker, no card. Then read how the full-game ladders sort by the handicap field, how to rank the books on an NBA board, and what the NBA player-prop menu holds. If you want the wide view first, the free-tier walkthrough covers auth, discovery and pagination in one pass.

FAQ

What does the period field return for basketball?

Ten values plus a null. result is the final score including overtime, fulltime is the 48-minute score, p1 to p4 are single quarters, p1+p2 is the first half, p3+p4 is the regulation second half and p3+p4+overtime is the second half with overtime. p4+overtime covers the fourth quarter with overtime. Six definitions, including Second Half Result, ship period: null.

How do I tell a regulation second-half total from an overtime one?

Read period on /v4/markets. p3+p4 settles on the third and fourth quarters alone; p3+p4+overtime adds anything scored after 48 minutes. The market name carries the same information in the suffix (incl. overtime), but a name normaliser that strips parentheses will merge the two ladders, so branch on the enum.

Do the two second-half ladders use the same line numbers?

Yes. Over Under Second Half holds 90 rungs from 40.5 to 129.5 in steps of a full point. Over Under Second Half (incl. overtime) holds 180 rungs from 40.5 to 130.0 in steps of half a point, and all 90 regulation rungs appear in it. Fourth-quarter totals behave the same way at 50 and 100 rungs.

Which bookmakers quote which clock?

It varies by book and by market. On San Antonio v New York on 14 June 2026, DraftKings priced the second half on both clocks, 25 market IDs on p3+p4 and 24 on p3+p4+overtime. Pinnacle quoted 17 second-half and 27 fourth-quarter IDs and used the overtime clock for all of them. Measure it per fixture rather than caching a map.

Can I get a regulation second-half spread?

The catalogue holds 90 second-half totals on the regulation clock and no spread or team total there. Handicap Second Half and the team totals exist on p3+p4+overtime with 201 and 400 definitions. The fourth quarter follows the same pattern, with a three-way result and a two-way winner added on p4.

How do I check a settled fixture for free?

/historical-odds returns the snapshot history for a played fixture on the free tier, capped at three bookmaker slugs a call, and /v4/scores returns each quarter as its own key. Together they let you replay a closing board and grade it on both clocks before you trust the resolver on a live game.