Props API: building an NFL player props model with live prop odds

By the propzapi team · Last updated July 2026 · 10 min read

Props API: build an NFL player props model with live player prop odds, 2026.

Player props are where the searches are, where the volume is, and where most odds APIs are weakest. That combination is worth building on.

An NFL player props model is a data pipeline before it's a model. You pull player prop odds as JSON from a props API, flatten them to one row per player, market and book, strip the vig out of each price, and compare the result to your own projection. The hard part isn't the math, it's getting clean, timestamped prop data across books in the first place.

This is a build guide, not betting advice. I'll show you the data shape, the code to pull and normalize it, an honest way to compare lines to projections, and the one benchmark that tells you whether any of it works. I use propzapi for the examples because it's what I build, but the pipeline is the same with any props-capable feed.

Why build on NFL props specifically?

Because it's the biggest market with the thinnest data coverage. The NFL drew roughly $30 billion in legal handle in 2025, about 37% of the entire US sports betting market, and books now post over 300 unique player props per Sunday slate compared with about 50 in 2019. Demand exploded, and most general odds APIs still treat props as an afterthought.

The scale is real. ESPN estimated legal NFL betting at a $30 billion high for 2025, inside a US market that took $165.58 billion in total handle per the American Gaming Association. One league, more than a third of everything.

Props are the part growing fastest inside that. Per WalterFootball's tracking of prop market growth, a single Sunday slate now carries 300-plus unique player props, up roughly six times from 2019.

Player props per NFL Sunday slate grew from about 50 in 2019 to over 300 in 2026, while same-game parlay hold runs far above straight-bet hold.
Props went from a side menu to the main course in about six years.

The books like props for a specific reason: margin. Same-game parlays, which are stacks of props, carry an effective hold in the range of 20% to 35%, against roughly 4.55% on a straight bet. That's why every book pushes them, and why prop coverage is the feature worth having in your data.

How is a player prop actually structured?

Three pieces: a player, a market, and a line. Patrick Mahomes, passing yards, 274.5. Each book then prices the over and the under on that number, usually near -110 a side. So a single "prop" is really one row per player, per market, per book, and if your storage flattens book away you've thrown out the only interesting part.

That last point is where people go wrong on day one. They store "Mahomes passing yards 274.5" as one row, then discover a week later that DraftKings had 274.5 and FanDuel had 271.5 and they can no longer tell which was which.

Anatomy of a player prop: a player, a market, a line, and then each book's over and under price on that line.
One prop, four parts. The book is a field, not a footnote.

The NFL markets worth pulling first, and what they price:

MarketWhat it pricesTypical line
player_passing_yardsQuarterback passing yards230.5 to 290.5
player_passing_tdsQuarterback passing touchdowns1.5 to 2.5
player_rushing_yardsRunning back rushing yards45.5 to 95.5
player_receptionsCatches for a receiver or back3.5 to 7.5
player_receiving_yardsReceiving yards35.5 to 85.5

Start with one market. Passing yards is the friendliest: few players per game, deep liquidity, and every book posts it.

How do you pull NFL props from the API?

One authenticated GET, filtered to the league and the market. You call the props endpoint with league=NFL and a market like player_passing_yards, and get back a tree of events, each holding its props, each prop holding every book's over and under. Filtering to the market you actually model keeps both the response and the credit cost small.

Prove the key works first.

curl "https://api.propzapi.com/v1/props?league=NFL&market=player_passing_yards" \
  -H "X-API-Key: $PROPZAPI_KEY"

The response is the shape everything else depends on. Events at the top, props inside, books inside those.

{
  "data": [
    {
      "event_id": "401671",
      "league": "NFL",
      "home_team": "Kansas City Chiefs",
      "away_team": "Buffalo Bills",
      "commence_time": "2026-09-13T17:00:00Z",
      "props": [
        {
          "player": "Patrick Mahomes",
          "market": "player_passing_yards",
          "line": 274.5,
          "books": [
            { "book": "DraftKings", "over": -115, "under": -105 },
            { "book": "FanDuel",    "over": -110, "under": -110 }
          ]
        }
      ]
    }
  ]
}
The props JSON tree: an event contains props, each prop has a player, market and line, and each prop holds per-book over and under prices.
Event, then prop, then book. Learn this once and every market reads the same.

Now flatten it. Nested JSON is fine to receive and miserable to model against, so collapse it to rows the moment it lands.

import requests

r = requests.get(
    "https://api.propzapi.com/v1/props",
    params={"league": "NFL", "market": "player_passing_yards"},
    headers={"X-API-Key": "YOUR_KEY"},
    timeout=15,
)
r.raise_for_status()
print("credits:", r.headers["X-Credits-Cost"])

# Flatten to one row per player / market / book. This is the shape
# everything downstream wants, and it keeps 'book' as a real field.
rows = []
for event in r.json()["data"]:
    for prop in event["props"]:
        for book in prop["books"]:
            rows.append({
                "event":  f"{event['away_team']} @ {event['home_team']}",
                "player": prop["player"],
                "market": prop["market"],
                "line":   prop["line"],
                "book":   book["book"],
                "over":   book["over"],
                "under":  book["under"],
            })

Watch the X-Credits-Cost header while you build. propzapi meters by market and returns the exact cost of every call, so you read what a request cost rather than guessing from a pricing page.

How do you compare a line to your own projection?

Strip the vig, then compare probabilities rather than raw numbers. Convert both sides of the book's price to implied probability, normalize them so they sum to 1, and you have the market's fair estimate. Your projection implies its own probability. The gap between the two is your signal, and the size of that gap is the only thing worth alerting on.

The vig is why you can't compare raw prices. A -110 over and a -110 under imply 52.4% each, which sums to 104.8%. That extra 4.8% is the book's margin, and it has to come out before the number means anything.

def implied(american):
    """American odds -> implied probability. Still includes the vig."""
    return (-american) / ((-american) + 100) if american < 0 else 100 / (american + 100)

def devig(over, under):
    """Normalize both sides to sum to 1, which strips the book's margin."""
    o, u = implied(over), implied(under)
    total = o + u
    return o / total, u / total

# Your own numbers, from whatever projection you trust.
projections = {"Patrick Mahomes": 289.0}

for row in rows:
    proj = projections.get(row["player"])
    if proj is None:
        continue
    fair_over, fair_under = devig(row["over"], row["under"])
    gap = proj - row["line"]
    print(f"{row['player']:<18} {row['book']:<12} line {row['line']:<7} "
          f"proj {proj:<7} gap {gap:+.1f}  mkt P(over) {fair_over:.3f}")
Join your projection to the book's line: de-vig the price to a fair probability, compare it to your projection, and flag the gap as a candidate edge.
The join is the model. Everything left of it is plumbing, everything right of it is discipline.

Where does the projection come from? That's your problem, and it's the part nobody can hand you. Common starting points are player usage rates, snap counts, target share, opponent defensive rank, pace, and weather. Start with something crude and honest rather than something clever and leaky.

And here's the discipline part. A gap between your number and the book's number is not edge. It's a hypothesis. The market is very good, especially on NFL passing yards, so most gaps mean your projection is wrong, not that the book is. The only way to find out is closing line value: log the line when you flag it, log the closing line, and check whether your flags consistently beat the close. If they don't, you have a bug, not an edge.

How do you turn the model into alerts?

Poll on an interval, keep the last line you saw per player and market, and fire when it changes or when your gap crosses a threshold. Prop lines drift slowly through the week and then move hard on injury news and inactives, so a simple in-memory diff catches almost everything that matters without any streaming infrastructure.

import time

seen = {}
while True:
    r = requests.get(URL, params=PARAMS, headers=HEADERS, timeout=15)
    r.raise_for_status()
    for event in r.json()["data"]:
        for prop in event["props"]:
            key = (prop["player"], prop["market"])
            if key in seen and seen[key] != prop["line"]:
                print("line moved:", key, seen[key], "->", prop["line"])
            seen[key] = prop["line"]
    time.sleep(60)   # pre-match cadence; tighten this as kickoff gets close

Tighten that interval as kickoff approaches. Sunday inactives land about ninety minutes before the first snap and props move fast on them, which is exactly when a slow poll makes you the last to know.

Alert flow: poll the props endpoint, diff each line against the last seen value, and fire an alert when a line moves or your projection gap crosses a threshold.
Poll, diff, alert. Keep the placing of any bet manual and human.

Send alerts to Discord, Telegram, or email. Keep the actual bet placement human: the retail books have no bet-placement API, and automating against them breaks their terms and gets accounts closed.

What breaks this pipeline in production?

Three things, all data problems. Un-timestamped snapshots make line moves invisible and invent edges that never existed. Flattening the book field away destroys the disagreement you're hunting. And scraping the books yourself means the feed dies mid-slate, which is the one time you cannot afford it.

The scraping one is worth being blunt about. Neither DraftKings nor FanDuel publishes a public API, so people reach for scrapers, and props are the hardest markets to scrape: hundreds of them per slate, rendered late by JavaScript, behind bot defenses. I wrote the full breakdown in odds API vs web scraping, but the short version is that scrapers break on redesigns and they break at peak.

Cache read-through and filter every request to the market you actually model. Pre-match props don't need second-by-second polling, live ones do, and you should never pay to pull markets you'll never show.

Frequently asked questions

How do I get NFL player prop odds via API?
Call a props endpoint filtered to the NFL and the market you want. With propzapi you send a GET to /v1/props with league=NFL and a market like player_passing_yards, and get back each player, the line, and every book's over/under price as JSON. There's a free tier with no card, so you can pull a slate and inspect the shape before building anything.
What is a player prop in data terms?
Three things: a player, a market, and a line. Patrick Mahomes (player), passing yards (market), 274.5 (line). Each book then prices the over and the under on that line, usually near -110 a side. So one prop is really one row per player, per market, per book, which is exactly how you should store it.
Why do books post different lines for the same player?
Because each book sets its own number from its own model and its own money. One book may hang Mahomes at 274.5 while another has 271.5, and the prices differ too. That disagreement is the entire reason to pull multiple books, and it's why your data layer must keep book as a field rather than flattening to one line.
How do I compare a prop line to my own projection?
Convert both sides of the book's price to implied probability, remove the vig by normalizing them to sum to 1, then compare that fair probability to what your projection implies. If your model says 289 passing yards and the book's line is 274.5, the gap is your signal. Whether it's real edge is a separate question that only closing line value answers.
How often do NFL prop lines move?
Slowly for days, then fast. Props drift through the week, then move hard on injury news, weather, and inactives, especially in the ninety minutes before kickoff when Sunday inactives land. Poll every few minutes pre-match and much faster near kickoff, and store every snapshot with a timestamp so you can see what moved and when.
Do I need historical prop data to build a model?
For evaluation, yes. You can build the pipeline on live props alone, but you cannot honestly test it without closing lines to grade against. Store the closing line for every prop from day one, because closing line value is the only benchmark that tells you whether your projection beat the market or just got lucky.

Pull one slate and look at it

Don't design the whole system first. Get a free key, run the cURL above for player_passing_yards on one NFL slate, and read the JSON. Flatten it to rows, print the lines from two books side by side, and you'll understand the data model better than any diagram. Then add one projection and see how often you disagree with the market. It'll be humbling, and that's the point.

The wider props walkthrough lives in how to get player props data via API, DraftKings and FanDuel specifics are in the DK and FD guide, and the fundamentals are in the complete odds API guide. Weighing providers on prop depth? The 2026 comparison covers it. Building for agents? There's an MCP server, and the docs have every market.

Get a free key Read the docs