Guide
Price NFL player props with a probability API in Python
Quick answer: Call /props?player_id=…&stat=rec&line=6.5 and you get the probability the player goes over the line, the fair American price for that probability, and the week's opponent and implied team total. Turn the price you're offered into its no-vig probability, subtract, and you have a number to rank props by. /defense adds what the opponent has allowed to that position. In a 2025–26 walk-forward backtest the model's probabilities matched real over rates within a few points (Brier 0.172 on passing yards against 0.250 for a base-rate guess).
- Endpoints
- /props, /defense
- Requests
- ~1 per prop
- Data
- 2025–26 box scores
- Plan
- Pro
Why turn a prop line into a probability?
A player prop is a yes/no question with a price attached: will this receiver catch more than 6.5 passes? The price already encodes a probability. At -110 on both sides each side implies 52.4%, and once you strip out the margin built into the two prices the market is saying 50%. The only interesting question is whether your own estimate of that probability is different, and by how much.
Getting that estimate is the hard part. You need a per-game expectation for the stat, a realistic spread around it (receiving yards swing far more than pass attempts), and a check that the probabilities it produces come true at the rate they claim. GridIron Data's /props endpoint does all three and returns the answer in one call, so the script in this guide is mostly bookkeeping:
- Price each line you're looking at with
/props. - Convert the offered prices into a no-vig market probability and compare.
- Add matchup context: the opponent's rank against that position from
/defense, and the team's implied points for the game. - Print the model's own track record for each stat, so you know how far to trust it.
What does the API give you for props?
GET /propstakes a player (player_id,espn_id, ornameplusposition), astatand one or morelinevalues. It returnsp_over,p_under,p_pushfor whole-number lines, fair American prices for both sides (fair_odds_over,fair_odds_under), the player's recent games for that stat, the week's game with its implied team total, and amodelblock with the stat's backtest.GET /defenseranks all 32 defenses by fantasy points allowed per game to QBs, RBs, WRs or TEs, with the yards, touchdowns, receptions and targets behind it.last=4limits it to the four most recent weeks. Rank 1 allows the most.GET /schedule(Basic) carries each game's spread, total, moneylines and both implied team totals, captured daily until kickoff and kept afterwards as the closing line.GET /players?espn_id=looks a player up by ESPN athlete id. Most third-party feeds key players that way, and every projection row and/propsanswer carriesespn_player_idas an integer, so joining another data source to ours is a dictionary lookup instead of fuzzy name matching.
The stat lines behind all of this come from ESPN box scores for every regular-season game since 2025, loaded each Monday and Tuesday morning after the week's games.
How do I price props in Python?
Set GRIDIRON_API_KEY to a Pro key, edit PROPS to the lines and prices you're looking at, and run it. The example uses -110 on both sides of every line so the market column reads 50%; with real prices that column moves.
import os
import requests
API = "https://api.gridirondata.com/api/v1"
H = {"x-api-key": os.environ.get("GRIDIRON_API_KEY", "YOUR_KEY")}
# Player, stat, line, price for the over, price for the under.
# Example lines at -110 both ways; put in the ones you're actually looking at.
PROPS = [
("Lamar Jackson#QB", "pass_yds", 214.5, -110, -110),
("Bijan Robinson#RB", "rush_yds", 79.5, -110, -110),
("Puka Nacua#WR", "rec", 6.5, -110, -110),
("Trey McBride#TE", "rec_yds", 64.5, -110, -110),
("Jahmyr Gibbs#RB", "rush_td", 0.5, -110, -110),
]
def get(path, **params):
r = requests.get(f"{API}{path}", headers=H, params=params, timeout=30)
r.raise_for_status()
return r.json()
def implied(price):
"""American price -> implied probability (vig included)."""
return 100 / (price + 100) if price > 0 else -price / (-price + 100)
def no_vig(over_price, under_price):
"""Market probability of the over with the bookmaker's margin removed."""
o, u = implied(over_price), implied(under_price)
return o / (o + u)
# One /defense call per position: last 4 weeks, rank 1 = allows the most.
defense = {}
for pos in {pid.split("#")[1] for pid, *_ in PROPS}:
rows = get("/defense", position=pos, last=4)["defenses"]
defense[pos] = {d["team"]: d["rank"] for d in rows}
print(f"{'player':20} {'stat':9} {'line':>6} {'model':>6} {'market':>7} {'diff':>6} "
f"{'fair over':>9} {'opp':4} {'opp rank':>8} {'team total':>10}")
for pid, stat, line, over_price, under_price in PROPS:
p = get("/props", player_id=pid, stat=stat, line=line)
game = p["game"] or {}
row = p["lines"][0]
market = no_vig(over_price, under_price)
pos = pid.split("#")[1]
rank = defense[pos].get(game.get("opponent"), "-")
total = game.get("implied_team_points")
flag = "" if p["available"] else f" ({p.get('unavailable_reason')})"
print(f"{p['player_name']:20} {stat:9} {line:6} {row['p_over']:6.1%} {market:7.1%} "
f"{row['p_over'] - market:+6.1%} {row['fair_odds_over'] or '-':>9} "
f"{game.get('opponent', '-'):4} {rank:>8} {total if total is not None else '-':>10}{flag}")
# How well has the model priced each stat? Walk-forward, 2025-26.
print(f"\n{'stat':9} {'graded':>7} {'brier':>6} {'base':>6} predicted -> actual over rate")
for stat in dict.fromkeys(s for _, s, *_ in PROPS):
pid = next(p for p, s, *_ in PROPS if s == stat)
bt = get("/props", player_id=pid, stat=stat, line=1)["model"]["backtest"]
cal = " ".join(f"{c['avg_predicted']:.0%}->{c['actual_over_rate']:.0%}" for c in bt["calibration"])
print(f"{stat:9} {bt['n']:7} {bt['brier']:6.3f} {bt['brier_base_rate']:6.3f} {cal}")
Two details are worth copying into your own code. Remove the margin before comparing: no_vig() rescales the two implied probabilities so they add to 100%; comparing the model to one side's raw implied probability makes every line look worse than it is. Leave week out: /props defaults to the current week and moves to the player's next game once this week's game is final, so a Thursday game doesn't keep getting priced on Friday.
What does the output look like?
Here's a run from October 23, 2026, ahead of the Week 7 Sunday games, with example lines at -110:
player stat line model market diff fair over opp opp rank team total
Lamar Jackson pass_yds 214.5 55.7% 50.0% +5.7% -126 CIN 20 25.0
Bijan Robinson rush_yds 79.5 58.4% 50.0% +8.4% -140 SF 11 23.5
Puka Nacua rec 6.5 62.5% 50.0% +12.5% -167 LV 5 26.0
Trey McBride rec_yds 64.5 39.1% 50.0% -10.9% 156 DEN 22 19.5
Jahmyr Gibbs rush_td 0.5 66.0% 50.0% +16.0% -194 GB 12 28.25
stat graded brier base predicted -> actual over rate
pass_yds 2135 0.172 0.250 12%->12% 28%->29% 50%->52% 72%->72% 88%->88%
rush_yds 4105 0.220 0.249 19%->15% 28%->31% 49%->53% 69%->72% 80%->50%
rec 8005 0.210 0.249 18%->17% 29%->32% 50%->52% 69%->70% 83%->81%
rec_yds 8435 0.223 0.247 19%->19% 29%->31% 49%->50% 68%->69% 80%->67%
rush_td 924 0.205 0.211 19%->22% 28%->27% 48%->43% 64%->54%
How to read the top table:
- model is
p_over; market is the no-vig probability from the two prices; diff is the gap between them. A positive diff means the model puts the over more likely than the prices do. - fair over is the price that would be exactly break-even if the model were right. Puka Nacua over 6.5 receptions at 62.5% is worth -167; anything shorter than that pays less than the model thinks it's worth.
- opp rank is the opponent's rank against that position over the last four weeks. Nacua faces the defense ranked 5th in fantasy points allowed to wide receivers; Trey McBride faces the 22nd against tight ends, and the model's 39.1% on his receiving yards line agrees that it's a tougher spot.
- team total is the team's implied points from the game's spread and total. Jahmyr Gibbs's 66.0% to score a rushing touchdown sits on a 28.25-point team total, the highest of the five.
The bottom table is the model grading itself. It replays 2025 and 2026, predicts each week using only earlier weeks, and checks lines from 0.6× to 1.4× each player's expectation. Read each pair as “when the model said X%, the over hit Y%”. Passing yards are close to exact across the board (12% to 12%, 50% to 52%, 88% to 88%). Two cautions: the top bucket for rushing and receiving yards is overconfident (80% predicted, 50% and 67% actual), and rushing touchdowns have the smallest sample and the weakest gain over the base rate (0.205 against 0.211). Treat very high probabilities on yardage, and anything on touchdowns, with extra skepticism.
How do I extend the script?
- Price a ladder of lines.
line=54.5,64.5,74.5returns all three in one call, which is how you'd price alternate lines or chart a player's whole distribution. - Join a feed by ESPN id. If your line data keys players by ESPN athlete id, pass
espn_id=to/propsinstead ofplayer_idand skip name matching entirely. - Watch for late scratches. A probability is only as good as the player's status. Pair this with the Pro
/injuries/changesfeed from our game-day injury changes guide and re-price anyone whose status flips. - Layer in usage. Our snap share guide shows how to spot a role change before the box score does; a receiver whose snap share just jumped is exactly the case a season-long expectation is slow to catch.
- Scan a whole position. Loop over
/projections/{week}rows for one position, read each player'sprojected_stats, and price lines set around them to find where the model and the prices disagree most.
What mistakes should I avoid?
- Comparing against the raw price. At -110 on both sides each side implies 52.4%. Compare the model against that and a fair line looks like a 2-point loser. Use the no-vig number.
- Treating a small diff as signal. The model is calibrated to within a few points in the middle of the range. A 2-point disagreement is inside that noise.
- Stacking related props. A quarterback's passing yards and his top receiver's receiving yards move together. Pricing them separately and multiplying the probabilities understates how often both go over.
- Ignoring availability. If
availableis false, every probability is 0 by design. Check it before reading the rest of the row. - Pricing too early. The model refits each Monday and Tuesday as box scores load, and statuses update through Sunday. The same line priced on Tuesday and on Sunday morning can come back different.
Frequently asked questions
Which plan do I need for /props and /defense?
Pro ($29/month, 50,000 requests a month). Free and Basic keys get a 403 with reason pro_required and an upgrade link. The per-stat weekly projections on /projections/{week} and the game lines on /schedule come with Basic ($5/month).
Which stats can I price?
Eleven: pass_yds, pass_td, pass_att, pass_cmp, pass_int, rush_yds, rush_att, rush_td, rec, rec_yds and rec_td, for quarterbacks, running backs, wide receivers and tight ends. Pass up to ten lines for one stat in a single call, comma-separated.
How is the probability calculated?
The mean is the player's season projection per game, shrunk toward his own average this season with six games of prior weight. The spread is fitted per position and stat on 2025-26 box scores of players with a real role. Receptions, receiving yards and rushing yards use a right-skewed gamma distribution, passing stats and rush attempts a normal, and touchdowns and interceptions a Poisson.
How accurate is it?
In a walk-forward backtest on 2025-26 games, each week predicted from earlier weeks only, the Brier score was 0.172 for passing yards against 0.250 for always guessing the base rate, and 0.205 to 0.223 for rushing and receiving stats against about 0.25. Predicted and actual over rates match within a few points in the middle of the range. Calibration says the probabilities are honest; it does not say they beat a market price.
What happens if a player is injured or on bye?
The response sets available to false with an unavailable_reason (BYE, OUT, IR, PUP, SUSPENDED, RESERVE or FREE_AGENT), and every p_over is 0. Statuses are checked against ESPN's injury report every 2 minutes.
How many requests does the script use?
One /props call per line you price, one /defense call per position, and one /props call per stat for the backtest table. The example prices five props in 14 requests.