NFL Player Props API: Fifteen Yardage Lines on One Receiver
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.