NFL Player Props API: Fifteen Yardage Lines on One Receiver

NFL Player Props API - OddsPapi API Blog
How To Guides September 9, 2026

You want NFL player props in JSON: anytime touchdown scorers, receiving yards, quarterback passing totals. You pull the fixture, walk the odds payload the way every game-line tutorial shows you, and the prop board comes back empty. Then you group what you do find by market ID and the numbers refuse to line up.

Both problems have the same root. The NFL prop board is not shaped like the moneyline board, and the code that reads one cannot read the other. This post walks the real structure using the Week 1 opener, Seattle Seahawks v New England Patriots, pulled live from the OddsPapi API on 31 August 2026. Every number below comes out of that payload.

Two Traps Before You Write a Line of Code

The first trap is the parser. On a game line, each outcome holds a players dict with a single key, "0". On a player prop, that same dict is keyed by player ID, and one outcome carries the whole depth chart at once. Hardcode players["0"] and the entire prop board reads as zero. We hit the same wall on MLB props and again on college football props.

The second trap is the one that costs you money, and it is specific to yardage markets. Cooper Kupp’s receiving yards carried 15 distinct active rungs on this fixture. DraftKings posted 33.5. Caesars and FanDuel posted 32.5. ESPNBet and theScore posted 34.5. Forty-six books sat on 29.5. Each rung is a separate market ID. Group your scanner by market ID and DraftKings gets compared to two offshore books and nobody else, while the 46-book rung looks like a deep consensus when it is mostly one European feed wearing a lot of brand names.

Cooper Kupp, receiving yards Books on the rung Two-sided and active
9.5 6 0
19.5 15 0
29.5 46 31
32.5 (Caesars, FanDuel) 5 5
33.5 (DraftKings) 3 3
34.5 (ESPNBet, theScore) 2 2
39.5 and above 12 to 16 each 1 to 2 each

The Old Way vs the OddsPapi Way

Task Scraping or a single-book API OddsPapi
Prop coverage One book per integration, per sport 99 books priced props on this one fixture, from a catalogue of 350+
Ladder rungs Whatever that book posts Every rung every book posts, with the handicap attached
Suspended lines Usually invisible in the HTML active on the price, suspended on the book
When the board opened Not recorded anywhere Free /historical-odds, full snapshot history
Duplicate feeds Counted as separate opinions Detectable by comparing the price tuple

Step 1: Authenticate and Find the Fixture

The API key is a query parameter, never a header. tournamentId=31 filters /fixtures to the NFL server-side and cuts the payload by roughly twenty times.

import requests, time
from collections import defaultdict

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

def call(path, **params):
    """apiKey is a query parameter. Honour retryMs on a 429."""
    params["apiKey"] = API_KEY
    for _ in range(5):
        r = requests.get(f"{BASE_URL}/{path}", params=params, timeout=600)
        if r.status_code == 429:
            time.sleep(r.json()["error"]["retryMs"] / 1000 + 0.3)
            continue
        r.raise_for_status()
        return r.json()
    raise RuntimeError("rate limited")

fixtures = call("fixtures", sportId=14, tournamentId=31,
                **{"from": "2026-09-09", "to": "2026-09-13"})
opener = sorted(fixtures, key=lambda f: f["startTime"])[0]
print(opener["fixtureId"], opener["startTime"])
# id1400003171515752 2026-09-10T00:20:00.000Z

Set to to the day after the last day you want. The window runs from midnight UTC to midnight UTC, so a same-day query returns only the fixtures that kick off at exactly 00:00Z.

Step 2: Build the Name Lookup

Market IDs and outcome IDs are integers. Names live in /markets. The sportId parameter does nothing there, so treat the response as one global lookup table.

catalog = call("markets", sportId=14)
market_name  = {m["marketId"]: m["marketName"] for m in catalog}
handicap     = {m["marketId"]: m.get("handicap") for m in catalog}
outcome_name = {(m["marketId"], o["outcomeId"]): o["outcomeName"]
                for m in catalog for o in m.get("outcomes", [])}

Step 3: Walk the Prop Tree

This generator is the whole parser. It skips the "0" key, drops the internal test feeds, and drops any book that has pulled the market while still returning prices.

odds  = call("odds", fixtureId=opener["fixtureId"])
board = odds["bookmakerOdds"]

def prop_prices(board):
    for slug, book in board.items():
        if slug.startswith("pinnacle+") or slug == "demo":
            continue                      # internal test feeds
        if book.get("suspended"):
            continue                      # book pulled the market
        for mid, market in book["markets"].items():
            for oid, outcome in market["outcomes"].items():
                for player_id, price in outcome["players"].items():
                    if player_id == "0":
                        continue          # "0" is the game line
                    yield slug, int(mid), int(oid), price

rows   = list(prop_prices(board))
active = [r for r in rows if r[3]["active"]]
print(f"{len(rows)} prop prices, {len(active)} active, {len(board)} books on the fixture")
# 8508 prop prices, 5391 active, 259 books on the fixture

Two hundred and fifty-nine books quote this fixture. Ninety-nine of them price player props. The rest stop at game lines.

Step 4: Resolve the Ladder

Key on the player name and the handicap together, and the rungs separate cleanly. The mainLine flag will not do this for you; it agreed with the consensus spread on only 46% of NFL flags when we measured it across the alt-line board.

FAMILY = "Over Under Player Receiving Yards (incl. overtime)"

rungs = defaultdict(lambda: defaultdict(dict))
for slug, mid, oid, price in prop_prices(board):
    if market_name.get(mid) != FAMILY or not price["active"]:
        continue
    side = outcome_name.get((mid, oid))
    rungs[price["playerName"]][handicap[mid]].setdefault(slug, {})[side] = price["price"]

player = "Kupp, Cooper"
print(f"{player}: {len(rungs[player])} distinct active rungs")
for line in sorted(rungs[player]):
    books = rungs[player][line]
    two   = [s for s, v in books.items() if "Over" in v and "Under" in v]
    print(f"  {line:>6}  {len(books):>3} books, {len(two):>2} two-sided")

# Kupp, Cooper: 15 distinct active rungs
#     9.5    6 books,  0 two-sided
#    14.5    2 books,  1 two-sided
#    19.5   15 books,  0 two-sided
#    24.5    2 books,  1 two-sided
#    29.5   46 books, 31 two-sided
#    32.5    5 books,  5 two-sided
#    33.5    3 books,  3 two-sided
#    34.5    2 books,  2 two-sided
#    39.5   16 books,  2 two-sided
#    ...

Note the two-sided column. Most rungs carry an Over and no Under, because books run a wide one-sided alt ladder around the line they actually want. You can only de-vig a rung that has both sides active.

Step 5: Dedupe Before You Average

Compare each book’s full prop tuple against every other book’s. Identical tuples mean one feed, several brands.

sig = defaultdict(list)
for slug in board:
    tup = tuple(sorted((m, o, p["playerName"], p["price"])
                       for s, m, o, p in prop_prices({slug: board[slug]})))
    if tup:
        sig[tup].append(slug)

print(f"{sum(len(g) for g in sig.values())} prop books -> {len(sig)} independent")
for g in sorted(sig.values(), key=len, reverse=True)[:3]:
    print(f"  {len(g)}x {sorted(g)[:5]}")

# 99 prop books -> 39 independent
#   21x ['7bet.co.uk', '7bet.lt', 'allbritishcasino', 'alphawin', 'betinia.dk']
#   10x ['leovegas.es', 'paf.es', 'prolineplus', 'scooore.be', 'unibet.be']
#    9x ['betcity.nl', 'betmgm.co.uk', 'betparx', 'betplay', 'betuk']

Ninety-nine books collapse to 39 independent prop opinions, a 60.6% reduction. The largest group runs 21 slugs deep. Eight Betano skins ship 180 identical prices each. Skip this step and a consensus line counts one trading desk twenty-one times.

The catalogue will not do the work for you. Of the 259 books on this fixture, 96 carry a cloneOf value, and they appear in the payload alongside their parents rather than hiding behind them. Plenty of the identical groups above are flagged cloneOf: null on every member. Compare the prices, not the metadata.

Step 6: De-Vig a Rung

line   = 29.5
quotes = {s: v for s, v in rungs[player][line].items()
          if "Over" in v and "Under" in v}
scored = sorted(((1/v["Over"] + 1/v["Under"]), s, v) for s, v in quotes.items())
overround, book, v = scored[0]

print(f"{player} {line} receiving yards, {len(quotes)} two-sided quotes")
print(f"  tightest {book}: Over {v['Over']} / Under {v['Under']} = {100*(overround-1):.2f}% margin")
print(f"  fair Over  {1/(1/v['Over']/overround):.3f}  ({100/v['Over']/overround:.1f}%)")
print(f"  fair Under {1/(1/v['Under']/overround):.3f}  ({100/v['Under']/overround:.1f}%)")

# Kupp, Cooper 29.5 receiving yards, 31 two-sided quotes
#   tightest apuestatotal: Over 1.87 / Under 1.87 = 6.95% margin
#   fair Over  2.000  (50.0%)
#   fair Under 2.000  (50.0%)

Thirty-one two-sided quotes on that rung, and the tightest still charges 6.95%. Props are expensive compared with the 2% to 4% you see on an NFL moneyline. Our no-vig guide covers the shift and multiplicative methods if the proportional one above is too blunt for your model.

Step 7: Anytime Touchdown, and the Second Market ID

Anytime touchdown scorer ships under two market IDs on this fixture, 14388 and 144637, with outcome names split across Yes, 1+, 2+ and 3+. Resolve on the market name and collect every ID that maps to it. Hardcode 14388 and you drop part of the board.

TD_IDS = {mid for mid, n in market_name.items()
          if n == "Player To Score TD (incl. overtime)"}

td = defaultdict(dict)
for slug, mid, oid, price in prop_prices(board):
    if mid in TD_IDS and outcome_name.get((mid, oid)) == "Yes" and price["active"]:
        td[price["playerName"]][slug] = price["price"]

for name, books in sorted(td.items(), key=lambda x: -len(x[1]))[:5]:
    hi = max(books.items(), key=lambda x: x[1])
    lo = min(books.items(), key=lambda x: x[1])
    print(f"  {name:<22} {len(books):>2} books  best {hi[1]:>5} ({hi[0]})"
          f"  worst {lo[1]:>5} ({lo[0]})  +{100*(hi[1]/lo[1]-1):.1f}%")

#   Shaheed, Rashid        25 books  best  4.36 (betfirst.be)  worst  3.25 (betano)  +34.2%
#   Maye, Drake            25 books  best  5.51 (betfirst.be)  worst   4.2 (superbet.bet.br)  +31.2%
#   Henry, Hunter          25 books  best 4.545 (kalshi)  worst   3.4 (betano)  +33.7%
#   Holani, George         25 books  best  3.66 (betfirst.be)  worst   2.5 (betano)  +46.4%
#   Kupp, Cooper           25 books  best  4.37 (betfirst.be)  worst   3.0 (fanduel)  +45.7%

Kupp to score at any point ran 3.00 at FanDuel and 4.37 at betfirst.be. That is a 45.7% gap in payout on the same bet. Anytime touchdown is a single-outcome market, so this is the best available price rather than a proven edge, and the two books may disagree because one has newer injury information. Either way, taking 3.00 when 4.37 is on the board is a decision you made by not looking. The same arithmetic drives our line shopping walkthrough.

Which Books Actually Price NFL Props

Ninety-nine of the 259 books on this fixture quoted at least one player prop. The list is US retail and European soft books. The sharps and the exchanges sat it out.

Book Prop prices Active Families Players
fanduel 321 91.6% 9 17
caesars / williamhill 250 84.8% 8 17
espnbet / thescore 233 96.1% 8 16
draftkings 200 88.5% 8 16
betano (8 skins) 180 each 85.0% 13 21
novig.us 174 86.2% 7 18
kalshi 110 100.0% 5 7
fanatics 81 66.7% 4 18
pinnacle, bet365, betmgm, betfair-ex, polymarket, sbobet 0 n/a n/a n/a

Pinnacle, Bet365 and BetMGM all quote this fixture. They quote the moneyline, the spread and the totals, and they stop there. If your model needs a sharp reference price on a receiving-yards line, the NFL prop board will not give you one. Kalshi is the interesting entry: a prediction market pricing 110 player props, and it posted the best anytime-touchdown number on Hunter Henry.

The Prop Board Runs on a Different Clock

Here is the finding that explains why your first pull came back empty. On this fixture, FanDuel’s game lines entered the feed on 9 August. Its player props entered on 31 August at 00:27:16 UTC, ten days before kickoff. Twenty-two days separate the two boards at the same book on the same game.

The free /historical-odds endpoint keeps the full snapshot history, so you can measure this yourself rather than guess.

hist = call("historical-odds", fixtureId=opener["fixtureId"], bookmakers="fanduel")
first = {}
for mid, market in hist["bookmakers"]["fanduel"]["markets"].items():
    for oid, outcome in market["outcomes"].items():
        for player_id, snaps in outcome["players"].items():
            if not snaps:
                continue
            key = "game line" if player_id == "0" else market_name.get(int(mid))
            opened = min(s["createdAt"] for s in snaps)
            first[key] = min(first.get(key, opened), opened)

for key, opened in sorted(first.items(), key=lambda x: x[1]):
    print(opened, key)

# 2026-08-09T00:27:50.819Z game line
# 2026-08-31T00:27:16.241Z Player To Score TD (incl. overtime)
# 2026-08-31T00:27:16.241Z Over Under Player Receiving Yards (incl. overtime)
# 2026-08-31T00:27:16.241Z Over Under Rush Yards (incl. overtime)
# 2026-08-31T00:27:16.241Z Over Under Pass Yards (incl. overtime)
# 2026-08-31T00:27:16.241Z Over Under Player Receptions (incl. overtime)
# 2026-08-31T00:27:16.241Z Over Under Player TD Passes (incl. overtime)
# 2026-08-31T00:27:16.241Z To Score TD First Half (incl. overtime)
# 2026-08-31T08:04:56.055Z Player To Score First TD (incl. overtime)

Seven families landed in the same second. That is a scheduled job firing, not a trader building a board market by market. First touchdown scorer followed 7.6 hours later.

Four more books did the same thing that morning. DraftKings opened at 00:31:48 UTC, Caesars and ESPNBet at 00:34:19, Kalshi at 00:57:40. Five independent operators put NFL Week 1 props up inside thirty minutes of each other.

Book Game lines first seen Props first seen
draftkings 2 Jun 2026 00:28 UTC 2 Jun 2026 00:48 UTC, pulled 4 Jun, back 31 Aug 00:31 UTC
fanduel 9 Aug 2026 00:27 UTC 31 Aug 2026 00:27 UTC
caesars 2 Jun 2026 00:45 UTC 31 Aug 2026 00:34 UTC
espnbet 2 Jun 2026 00:45 UTC 31 Aug 2026 00:34 UTC
kalshi 2 Jun 2026 00:41 UTC 31 Aug 2026 00:57 UTC

DraftKings leaves one earlier fragment, and it is worth reading properly. It posted an anytime-touchdown board on 2 June, held it for three days, and pulled it: 60 snapshots across 2 to 4 June, then nothing at all until 31 August. That 2 June date is also exactly where NFL price collection begins, 100 days before kickoff, so treat it as the edge of the recording window rather than the moment a trader opened the board.

The gap is the part that matters. For the twelve weeks between those two dates, this fixture had no player props at any book. Pull it in mid-August and you get 259 books, a full game-line board and an empty prop board, which is exactly what the other fifteen Week 1 fixtures still returned on 31 August. The 31 August timestamps carry no ambiguity, because those markets appeared inside a window we were already recording.

The practical rule: schedule your prop scanner against kickoff, not against the calendar. Poll a fixture ten days out and you will find the board mid-build. Poll it two weeks out and you will find game lines only, which is exactly what the other fifteen Week 1 fixtures returned on the same afternoon. Our 272-fixture study of the NFL game-line board maps the same clock for spreads and totals.

The Twenty-One Prop Families, and How Alive They Are

Active rate splits the board in two. Touchdown markets stay open. Yardage ladders sit mostly suspended, because a book posts twenty rungs and takes bets on three.

Family Prices Active Books Players Market IDs
Over Under Player Receiving Yards 2,176 62.5% 91 8 41
Over Under Rush Yards 1,147 53.9% 91 5 31
Over Under Pass Yards 888 41.1% 90 2 35
Player To Score TD 830 91.1% 32 23 2
Over Under Player Receptions 801 49.8% 52 8 10
Player To Score First TD 581 92.8% 34 23 1
Over Under Player TD Passes 410 60.0% 88 2 3
Rush Yards (threshold ladder) 338 16.9% 37 5 1
Over Under Rush TD 330 100.0% 38 5 1
Pass Yards (threshold ladder) 275 23.6% 38 2 1
Over Under Player TD 153 94.8% 6 20 3
To Score TD Second Half 132 100.0% 7 18 1
To Score TD First Half 126 100.0% 7 17 1
To Score TD First Quarter 75 86.7% 5 13 1
Over Under Player Interceptions 56 100.0% 14 2 1
Over Under Longest Rush Yards 48 100.0% 8 3 2
Over Under Longest Pass Completion 48 66.7% 8 2 2
Over Under Field Goals 36 100.0% 9 2 1
Over Under Kicking Points 32 100.0% 8 2 2
Over Under Pass Attempts 22 0.0% 6 2 2
Over Under Pass Completions 22 0.0% 6 2 2

Read the players column next to the books column. Receiving yards runs 91 books deep and 8 players wide. Anytime touchdown runs 32 books deep and 23 players wide. Books price the whole roster for a binary touchdown question and only the primary targets for a continuous yardage question, because a yardage line on a fourth receiver is a liability with no volume behind it.

Two families ship with every price suspended. Pass attempts and pass completions are posted and closed. Filter on active at the price level or your scanner will report 44 quotes that nobody will take.

Note the two naming conventions. Over Under Rush Yards is a two-sided market with a handicap, and you can de-vig it. Bare Rush Yards is a one-sided threshold ladder on a single market ID, with outcomes like 60+ and 90+, and you cannot. Treat them as different market types even though the names look almost identical.

What to Build With This

The ladder resolver and the dedupe pass are the two pieces most prop scanners skip, and they are the two that decide whether your output means anything. Once both are in place the rest is arithmetic: de-vig the deepest two-sided rung to get a fair line, compare each book’s own rung against it, and rank by the gap. Our player props value scanner walks that loop end to end, and the backtesting guide shows how to grade it against closing prices from the free historical endpoint. If you want a ready-made screener rather than your own, jedibets.com runs one over player props with Discord alerts.

Point it at the right fixture, at the right time, and check what the board is actually made of. Ninety-nine books, 39 opinions, and a receiver with fifteen different lines on him.

FAQ

Is there an official NFL player props API?

No. The NFL does not publish odds, and the sportsbooks that price props closed their public feeds years ago. DraftKings, FanDuel and Caesars all require a commercial partnership. OddsPapi aggregates the prop board from 99 books on a single Week 1 fixture through one REST call, with a free tier.

Which bookmakers price NFL player props?

On the Seattle v New England opener, FanDuel led with 321 prop prices, followed by Caesars and William Hill at 250, ESPNBet and theScore at 233, and DraftKings at 200. Eight Betano skins carried 180 each. Kalshi priced 110 props, which is unusual for a prediction market. Pinnacle, Bet365 and BetMGM quoted the game lines on this fixture and left the prop board alone.

Why do NFL props from different books look impossible to compare?

Because each rung of a yardage ladder is its own market ID. Cooper Kupp’s receiving yards carried 15 distinct active rungs on one fixture, with DraftKings on 33.5, Caesars and FanDuel on 32.5, and ESPNBet on 34.5. Group by market ID and you compare books that are not quoting the same bet. Key on the player name plus the handicap value instead.

When do NFL player props open?

Later than the game lines, and by a wide margin. On the Week 1 opener, FanDuel posted game lines on 9 August and props on 31 August at 00:27 UTC, ten days before kickoff. DraftKings followed at 00:31 UTC, Caesars and ESPNBet at 00:34, and Kalshi at 00:57, all on the same morning. Schedule any prop job relative to kickoff rather than on a fixed calendar.

How do I get the player name out of the odds payload?

On a player prop the players dict is keyed by player ID, not by "0", and each entry carries a playerName field formatted as "Last, First". One outcome holds the whole set of priced players. Iterating the dict and skipping the "0" key returns everything.

Can I backtest NFL props with free historical data?

Yes. /historical-odds returns the full snapshot history for a fixture on the free tier, capped at three bookmakers per call. That is how the open times in this post were measured. Snapshots keep recording past kickoff, so filter on createdAt before startTime to get a genuine closing line.

Get Your Free API Key

Stop scraping prop pages that change their DOM every fortnight. One REST call returns 99 prop books, 21 families and 144 market IDs on a single NFL fixture, alongside 350+ bookmakers across every other sport, plus historical snapshots that competitors charge for. Get your free OddsPapi API key and read the free tier guide to see what fits inside it.