NFL Sportsbook Margins: Rank 244 Books on Every Week 1 Market

NFL Sportsbook Margins - OddsPapi API Blog
How To Guides September 10, 2026

Ask which sportsbook prices NFL Week 1 tightest and you get a different answer for every market on the board. We pulled all 16 Week 1 fixtures on 1 September 2026, ranked 244 bookmakers by margin, and found the leaderboard reorders itself between the moneyline and the spread.

Betfair Exchange sits 6th of 244 on the Week 1 moneyline at a 2.55% median margin. On the point spread the same feed ranks 216th of 223 at 10.54%. SBOBet runs the other way: 149th on the moneyline, 14th on the spread, 6th on the total.

If your scanner grades books on moneyline vig and then bets spreads, it is grading the wrong market.

What We Measured

One /odds call per fixture, no bookmaker filter, on 1 September 2026 at T-9 days from kick-off. Sixteen fixtures, 1,140,165 prices, 263 distinct bookmakers, 1,950 market IDs and 80 market families. The median board carried 258 books per fixture.

Slate figure Value
Fixtures 16 (Sep 10 to Sep 15, 2026)
Median bookmakers per fixture 258
Total prices 1,140,165
Active prices 436,005 (38.2%)
Two-sided active moneylines, median 240 per fixture
Independent moneyline prices, median 63 per fixture
Distinct market IDs 1,950

Only 38.2% of those prices carry active: true. That number is not a defect. A wide board posts a deep ladder and suspends most of it, and the same inversion shows up on tennis and rugby. Filter on the price-level flag before you rank anything.

Why Checking Five Books Gives You the Wrong Answer

The usual margin comparison opens five US sportsbook tabs, reads the moneyline, and calls the tightest one sharp. Two problems break that method on an NFL board.

First, five books is a sample of one price. Dedupe the Week 1 opener on the price tuple and 230 two-sided moneyline quotes collapse to 57 independent prices, a 75.2% collapse. The largest single group is 27 slugs quoting 1.52 / 2.60. DraftKings, FanDuel, Caesars and BetMGM sit inside four different groups, so a five-book comparison samples four feeds and calls it a market.

Second, the moneyline barely predicts the rest of the board. Rank the 221 books that quote all three main markets and the moneyline order correlates with the spread order at r = 0.59. Spread and total correlate at r = 0.94. There are two boards here: the moneyline, and everything else.

The Old Way With OddsPapi
Five browser tabs, one market One call, 258 books, 80 market families
Clone feeds counted as separate opinions Dedupe on the price tuple before you count
Moneyline vig treated as a book-wide score Per-family margin, per book, across 16 fixtures
Suspended boards read as live prices Book-level suspended plus price-level active
Sharps and prediction markets missing Pinnacle, SBOBet, Betfair, Polymarket, Kalshi in the same payload

Build the Margin Leaderboard in Python

Step 1: Authenticate and pull Week 1

The API key is a query parameter. NFL is tournamentId 31 inside sportId 14, which also carries NCAA, CFL and Arena Football. An empty date window answers HTTP 404 rather than an empty list, so catch it.

import requests, time, statistics

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

def week1_fixtures():
    r = requests.get(f"{BASE_URL}/fixtures", params={
        "apiKey": API_KEY, "sportId": 14, "tournamentId": 31,
        "from": "2026-09-10", "to": "2026-09-16",
    })
    if r.status_code == 404:      # empty window, not an error
        return []
    r.raise_for_status()
    return r.json()

fixtures = week1_fixtures()
print(len(fixtures), fixtures[0]["participant1Name"])
# 16 Seattle Seahawks

Set to to the day after your last day. The bound is a midnight-UTC instant, so a same-day window returns only the games that start at exactly 00:00Z.

Step 2: Resolve markets by name, never by ID

NFL spreads and totals carry one market ID per line. The -3.5 spread is 14272, the -4 is 14270, the 44.5 total is 1464. Hardcode an ID and you pin yourself to a line the book may not quote. Pull the catalogue once and index it by name.

def market_index(sport_id=14):
    cat = requests.get(f"{BASE_URL}/markets",
                       params={"apiKey": API_KEY, "sportId": sport_id}).json()
    names = {m["marketId"]: m["marketName"] for m in cat}
    handicaps = {m["marketId"]: m.get("handicap") for m in cat}
    return names, handicaps

MARKET_NAME, MARKET_HANDICAP = market_index()
print(MARKET_NAME[141], MARKET_HANDICAP[14272])
# Winner (incl. overtime) -3.5

The three families this post ranks are Winner (incl. overtime), Handicap (incl. overtime) and Total (incl. overtime). The sportId parameter is a no-op, so the same 32,815-row catalogue comes back whatever you pass. Use it as a lookup table only.

Step 3: Fetch one board

def board(fixture_id):
    for _ in range(4):
        r = requests.get(f"{BASE_URL}/odds",
                         params={"apiKey": API_KEY, "fixtureId": fixture_id},
                         timeout=300)
        if r.status_code == 200:
            return r.json().get("bookmakerOdds", {})
        if r.status_code == 429:
            time.sleep(r.json()["error"]["retryMs"] / 1000 + 0.5)
            continue
        r.raise_for_status()
    return {}

books = board(fixtures[0]["fixtureId"])
print(len(books), "books")
# 262 books

A 429 response is valid JSON with a retryMs field. Code that only looks for bookmakerOdds reads a rate-limit reply as an empty board. Sleep one second between calls on the same endpoint and do not run /odds in threads.

Step 4: Apply all three filters

Three separate things can make a price unusable, and they live at different levels of the payload.

TEST_FEEDS = {"demo", "singbet-b"}

def tradable(slug, book):
    if slug.startswith("pinnacle+") or slug in TEST_FEEDS:
        return False                              # internal test feeds
    if book.get("suspended") or book.get("bookmakerIsActive") is False:
        return False                              # book pulled its board
    return True

def price(outcome):
    p = outcome.get("players", {}).get("0")
    if not isinstance(p, dict) or p.get("active") is not True:
        return None                               # price-level suspension
    v = p.get("price")
    return v if isinstance(v, (int, float)) and v > 1 else None

live = {s: b for s, b in books.items() if tradable(s, b)}
print(len(books), "->", len(live))
# 262 -> 236

Across the slate, a median of 14 books per fixture ship suspended: true while still returning prices. Kalshi is one of them on 15 of 16 fixtures, quoting 67 markets on the opener with the book-level flag set. Read all three levels and pick your own policy. This study excludes suspended books, so Kalshi does not appear in the tables below.

Step 5: Rank the moneyline

def moneyline_quotes(live_books):
    out = {}
    for slug, book in live_books.items():
        for market_id, market in book["markets"].items():
            if MARKET_NAME.get(int(market_id)) != "Winner (incl. overtime)":
                continue
            legs = sorted(market["outcomes"], key=int)
            prices = [price(market["outcomes"][o]) for o in legs]
            if len(prices) == 2 and all(prices):
                out[slug] = tuple(prices)
    return out

margin = lambda t: sum(1 / p for p in t) - 1
quotes = moneyline_quotes(live)

for slug, t in sorted(quotes.items(), key=lambda kv: margin(kv[1]))[:5]:
    print(f"{slug:16s} {t[0]:6.3f} / {t[1]:6.3f}   {margin(t)*100:5.2f}%")
# polymarket.us     1.600 /  2.632    0.49%
# prophetx          1.575 /  2.680    0.81%
# novig.us          1.562 /  2.703    1.02%
# polymarket        1.562 /  2.632    2.01%
# duel              1.550 /  2.660    2.11%

Every book in that top five is a peer-to-peer venue. Read post 3713 on NFL prediction market liquidity before you treat those numbers as prices you can bet: Polymarket US medians a 0.50% margin with $0.00 of depth on both sides.

Step 6: Resolve the ladder before you rank it

Spreads and totals arrive as a ladder of separate market IDs. The mainLine flag does not identify the headline rung on any sport we have measured, and on NFL it matched the resolved line on 46.1% of spread flags. Resolve each book’s own line instead: take the rung whose two prices sit closest together, inside a sane price band.

def balanced_rung(book, family, lo=1.5, hi=2.6):
    rungs = []
    for market_id, market in book["markets"].items():
        if MARKET_NAME.get(int(market_id)) != family:
            continue
        prices = [price(o) for o in market["outcomes"].values()]
        prices = [p for p in prices if p]
        if len(prices) == 2 and all(lo <= p <= hi for p in prices):
            rungs.append((abs(prices[0] - prices[1]),
                          margin(tuple(prices)),
                          MARKET_HANDICAP.get(int(market_id))))
    return min(rungs) if rungs else None

for slug in ("pinnacle", "betfair-ex", "draftkings"):
    s = balanced_rung(live[slug], "Handicap (incl. overtime)")
    t = balanced_rung(live[slug], "Total (incl. overtime)")
    print(f"{slug:12s} spread {s[2]} {s[1]*100:5.2f}%   total {t[2]} {t[1]*100:5.2f}%")
# pinnacle     spread -3.5  2.88%   total 44.0  4.34%
# betfair-ex   spread -3.5  2.31%   total 44.5  9.01%
# draftkings   spread -3.5  4.71%   total 44.5  4.71%

The lo and hi band earns its place. Betfair Exchange posted six spread rungs on the opener priced 1.01 on both sides, all flagged active: true. Those are empty books, and they are the most balanced pair on the ladder by price difference. Without the band the resolver picks one and reports a 98% margin.

Step 7: Dedupe before you count

groups = {}
for slug, t in quotes.items():
    groups.setdefault(t, []).append(slug)

biggest = max(groups.values(), key=len)
print(f"{len(quotes)} quotes -> {len(groups)} independent "
      f"({100*(1-len(groups)/len(quotes)):.1f}% collapse), biggest group {len(biggest)}")
# 231 quotes -> 57 independent (75.3% collapse), biggest group 27

Ignore the cloneOf field for this. It predicts neither presence nor duplication: 96 of the 262 books on the opener carry a cloneOf value and ship anyway, while Caesars and William Hill are both flagged null and quote the same tuple to four decimal places. The price tuple is the only reliable test.

The Week 1 Margin Leaderboard

Median margin per book across the 16 fixtures, suspended books and internal test feeds removed, on the 244 books that quoted at least 10 of 16 moneylines.

# Book Moneyline margin Type
1 polymarket.us 0.99% Prediction market
2 polymarket 1.02% Prediction market
3 prophetx 1.75% Exchange
4 duel 2.03% Exchange
5 novig.us 2.25% Exchange
6 betfair-ex 2.55% Exchange
20 pinnacle 3.17% Sharp
28 circasports 3.41% US retail
42 fanduel 4.10% US retail
59 bet365 4.18% Global retail
74 draftkings 4.31% US retail
106 betmgm 4.55% US retail
149 sbobet 5.52% Sharp Asian
242 fdj 13.05% French retail
243 unibet.fr 13.05% French retail
244 zebet.fr 13.05% French retail

The three French slugs are one feed. fdj and zebet.fr both carry cloneOf: unibet.fr and quote 1.440 / 2.300 on the opener where the board medians 1.511 / 2.550. Betting the favourite there costs 4.7% against the market median.

The Rank Does Not Carry Across Families

Now run the same books through the spread and the total. This is the table the moneyline leaderboard hides.

Book Moneyline Spread Total
polymarket 1.02% (#2) 1.99% (#1) 3.99% (#4)
prophetx 1.75% (#3) 2.43% (#2) 3.84% (#3)
polymarket.us 0.99% (#1) 4.00% (#15) 4.00% (#5)
pinnacle 3.17% (#20) 3.08% (#9) 4.20% (#12)
sbobet 5.52% (#149) 3.76% (#14) 4.18% (#6)
coolbet 6.04% (#155) 4.16% (#16) 4.30% (#15)
fanduel 4.10% (#42) 4.76% (#79) 4.72% (#61)
circasports 3.41% (#28) 4.77% (#84) 4.77% (#81)
betfair-ex 2.55% (#6) 10.54% (#216) 10.68% (#221)
sharpxch 2.55% (#10) 10.54% (#220) 10.68% (#225)

Three patterns come out of that table.

The exchange collapses off the moneyline. Betfair and its four clone slugs give up 210 rank places between the moneyline and the spread. The exchange quotes a two-way match-winner market with real money in it and a handicap ladder that almost nobody trades, and the back-only price widens to match. On 11 of 16 fixtures its resolved spread rung priced above a 9% margin, most of them at 1.78 / 1.83.

SBOBet inverts. It is 149th on the moneyline and 14th on the spread, which repeats what we found on SBOBet’s soccer board: it parks the straight winner market and trades the handicap. Coolbet does the same thing, 155th to 16th.

US retail books drift the other way. Circa Sports is 28th on the moneyline and 84th on the spread. FanDuel goes 42nd to 79th. The tight moneyline is a shop window; the spread is where the hold lives.

Of the 221 books that quote all three families, only ten rank inside the top 20 of every one, and six of those ten are Pinnacle skins. Pinnacle ships under eight slugs on this board (pinnacle, pin88, ps3838, asports.bet, bet487, bolsadeaposta-spb, ole777, piwi247-spb) quoting an identical 1.552 / 2.580 on the opener. Strip the duplicates and five feeds hold rank across the whole board: Pinnacle, Polymarket, Polymarket US, ProphetX and Duel.

What the Best Price Is Worth

Margin ranks a book. It does not tell you what to bet. Across the 32 moneyline sides in Week 1, the best available price beat the board median by 6.23% at the median and 12.50% at the maximum.

On the opener, Seattle at home to New England:

Side Best price Book Board median Gain
Seattle 1.600 polymarket.us 1.511 +5.86%
New England 2.703 novig.us 2.550 +6.00%

Who owns those best prices matters as much as the size of the gain. Across all 32 sides, polymarket.us held the top price on 9 and betdsi on 6. Polymarket US publishes $0.00 of ladder depth on NFL, and BetDSI is flagged liveOdds: false in the catalogue. Pair every best-price scan with a depth check and an account check, or you will build a screen full of numbers you cannot take. The line shopping guide covers the account side.

How Many Books Do You Actually Need

Sixteen fixtures at 258 books is 16 calls and about 400 MB. Most jobs do not need that. Our 176-book closing-line study put the consensus knee at 25 independent books, and showed that a deliberate three-book set beats a random 25. For an NFL margin leaderboard the answer is different again, because the thing you are measuring is the spread of the field. Pull the full board once, dedupe, and cache the group map. The clone groups are stable across a slate.

Frequently Asked Questions

Which sportsbook has the lowest NFL margin?

On the Week 1 2026 moneyline the tightest quotes came from peer-to-peer venues: Polymarket US at 0.99% and Polymarket at 1.02%, ahead of ProphetX at 1.75%. Among sportsbooks, Pinnacle led at 3.17% and ranked 20th of 244 overall. On the point spread the order changes: Polymarket 1.99%, ProphetX 2.43%, Pinnacle 3.08%.

Why does one book price the moneyline tightly and the spread wide?

Liquidity follows the headline market. Betfair Exchange medians 2.55% on the Week 1 moneyline and 10.54% on the spread because its handicap ladder carries far less matched money. Sportsbooks show the reverse pattern, tightening the moneyline that shoppers compare and holding more on derivative markets.

Can I filter the /odds endpoint to one bookmaker?

Yes, with bookmakers=<slug>. A slug outside the catalogue returns HTTP 400 and the error body lists every valid slug. A slug your key cannot read returns HTTP 403. A valid slug that is simply not on that fixture returns HTTP 200 with the slug absent, so use .get() rather than indexing.

Does the mainLine flag identify the headline spread?

Not reliably. Across the NFL Week 1 slate it matched the resolved main line on 46.1% of spread flags and 35.7% of total flags, and several books flag every rung. Resolve each book’s own most balanced rung, then take the mode across books.

How often do NFL bookmakers quote identical prices?

Constantly. The Week 1 opener carried 230 two-sided active moneyline quotes that collapse to 57 independent prices, and the largest group is 27 slugs on one number. Dedupe on the price tuple before averaging, because cloneOf flags neither all of the duplicates nor only the duplicates.

Get the Board

OddsPapi aggregates 350+ bookmakers through one JSON endpoint: sharps like Pinnacle and SBOBet, exchanges, prediction markets, and the European retail books that never show up in a US comparison. Native market structures mean Asian handicaps, alternate ladders and player props arrive parsed rather than scraped, and historical price history is on the free tier so you can replay how a margin moved.

Stop reading five tabs. Get your free API key and rank the whole board.

Related reading: the NFL odds API guide covers lines, spreads and totals from scratch; the vig calculator and no-vig odds posts cover the margin maths; NFL alternate lines maps the full ladder; and the Bwin study shows one brand shipping seven separately priced feeds.