Player Props API: How to Get NFL, NBA & MLB Prop Odds in Python
Every Player Props API Gives You 40 Bookmakers. Here Are 350+.
You need player prop odds for your model, your scanner, or your +EV tool. You go to The Odds API — 40 bookmakers. You try SportsGameOdds — 85 books, but the free tier gives you 9 with a 10-minute delay. Neither has Singbet or SBOBet. Neither shows you what the JSON actually looks like.
SportsGameOdds has written four blog posts about player props. Not one of them includes a JSON response structure or a working code example. The Odds API’s player props post is 800 words of marketing copy.
This post is different. You’ll get:
- Working Python code that fetches real player prop odds from 350+ bookmakers
- The actual JSON structure — so you can parse it without guessing
- Native combo props (PRA, Double-Double, Triple-Double) as first-class markets
- Cross-bookmaker line shopping for props
- Free historical prop odds for backtesting your models
Let’s build.
Player Props: Old Way vs OddsPapi
| Feature | Scraping | The Odds API | SportsGameOdds | OddsPapi |
|---|---|---|---|---|
| Bookmakers with Props | 1-2 sites | ~15 | ~85 (9 on free) | 350+ |
| Sharp Books (Pinnacle, Singbet) | ❌ | ❌ | Pinnacle only | All three (+ SBOBet) |
| Combo Props (PRA, Double-Double) | Manual scrape | Limited | Generic | Native market IDs |
| JSON Response Shown in Docs | N/A | ❌ | ❌ | ✅ (this post) |
| Historical Prop Odds | ❌ | Paid add-on | Pro tier ($299+/mo) | Free tier |
| Authentication | N/A | Header token | Header token | Query param |
| Free Tier Latency | N/A | ~60s delay | 10-min delay | Real-time |
What Are Player Props in API Terms?
Player props are markets tied to individual player performance, not the game outcome. Points scored, rebounds grabbed, passing yards thrown, strikeouts pitched. In OddsPapi’s data model, player props work like team markets with two key differences:
- The
playerNamefield in the outcome is non-null (e.g., “Cunningham, Cade”) - The
playersdict uses player IDs as keys (e.g.,"2068637") — not"0"like team markets
Here’s what’s available per sport:
| Sport | Market Types | Example Props | Market IDs in Catalog |
|---|---|---|---|
| NBA | 29 | Points, Rebounds, Assists, PRA, 3-Pointers, Steals, Blocks, Double-Double, Triple-Double | 439 |
| NFL | 32 | Pass Yards, Rush Yards, Receiving Yards, TDs (Anytime, First, 2nd, 3rd), Pass Completions, Tackles | 1,243 |
| MLB | 30 | Strikeouts, Hits, Runs, RBIs, Total Bases, Home Runs, Stolen Bases, Walks, Outs | 122 |
That is not a typo. A single NFL fixture can have 1,243 player prop market IDs across 32 market types — each handicap line (Over 249.5 yards, Over 250.5 yards) gets its own unique market ID.
Tutorial: Fetching Player Prop Odds with Python
Step 1 — Authentication
Install requests if you haven’t, then verify your connection:
import requests
import time
API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.oddspapi.io/v4"
def api_get(endpoint, **params):
params["apiKey"] = API_KEY
resp = requests.get(f"{BASE_URL}/{endpoint}", params=params)
resp.raise_for_status()
return resp.json()
# Verify connection
sports = api_get("sports")
print(f"Connected — {len(sports)} sports available")
Important: apiKey is a query parameter, not a header. This trips up developers migrating from other APIs. Full API docs here.
Step 2 — Discovering Player Prop Markets
Before you can fetch prop odds, you need to know the market IDs. The /markets endpoint gives you the full catalog for any sport:
# Fetch the NBA market catalog
catalog = api_get("markets", sportId=11)
# Filter for player prop markets
prop_keywords = ["Player", "First Basket", "Double", "Triple"]
prop_markets = [
m for m in catalog
if any(kw in m["marketName"] for kw in prop_keywords)
]
# Build lookup dicts
market_names = {m["marketId"]: m["marketName"] for m in catalog}
outcome_names = {
(m["marketId"], o["outcomeId"]): o["outcomeName"]
for m in catalog for o in m.get("outcomes", [])
}
# Show unique prop types
prop_types = sorted(set(m["marketName"] for m in prop_markets))
print(f"{len(prop_types)} player prop types in NBA:")
for name in prop_types[:10]:
example = next(m for m in prop_markets if m["marketName"] == name)
print(f" {name} (e.g. ID {example['marketId']}, line {example.get('handicap', 0)})")
Each handicap line has its own marketId. “Over Under Player Points” at line 17.5 and at line 19.5 are different market IDs. The handicap field tells you the line. Don’t hardcode market IDs — build lookups from the catalog.
Step 3 — Fetching Prop Odds for a Fixture
Find an NBA game and pull live prop odds from multiple bookmakers:
# Find NBA fixtures with odds
fixtures = api_get("fixtures", sportId=11, **{
"from": "2026-04-12",
"to": "2026-04-15"
})
nba_with_odds = [
f for f in fixtures
if f.get("hasOdds") and "nba" in f.get("tournamentSlug", "").lower()
]
print(f"NBA fixtures with odds: {len(nba_with_odds)}")
# Pick the first fixture
fixture = nba_with_odds[0]
fid = fixture["fixtureId"]
print(f"{fixture['participant1Name']} vs {fixture['participant2Name']}")
time.sleep(0.2) # Rate limit safety
# Fetch odds from multiple bookmakers
odds = api_get("odds", fixtureId=fid,
bookmakers="hardrockbet,fanduel,thescore,betrivers,caesars,pinnacle")
# Check which bookmakers responded
bk_odds = odds.get("bookmakerOdds", {})
print(f"Bookmakers in response: {list(bk_odds.keys())}")
hasOdds: true will return odds.Step 4 — Parsing Props into a Clean Table
Here’s the key: for player props, the players dict uses player IDs as keys (not "0"), and playerName is populated. Here’s what a player prop outcome looks like in the raw JSON:
# Raw JSON structure for a player prop outcome:
# response["bookmakerOdds"]["hardrockbet"]["markets"]["111678"]["outcomes"]["111678"]["players"]
# {
# "2068637": {
# "active": true,
# "betslip": "https://app.hardrock.bet/?deep_link_value=betslip/...",
# "bookmakerOutcomeId": "5383800863007572306",
# "changedAt": "2026-04-12T10:08:45.327607+00:00",
# "limit": null,
# "playerName": "Cunningham, Cade",
# "price": 1.625,
# "exchangeMeta": {}
# }
# }
Now let’s parse all player props across every bookmaker into a clean table:
# Parse all player props from the odds response
props = []
for bk_slug, bk_data in bk_odds.items():
for mid_str, mdata in bk_data.get("markets", {}).items():
mid = int(mid_str)
mname = market_names.get(mid, "Unknown")
for oid_str, odata in mdata.get("outcomes", {}).items():
oid = int(oid_str)
oname = outcome_names.get((mid, oid), oid_str)
for player_id, pdata in odata.get("players", {}).items():
if not isinstance(pdata, dict):
continue
if not pdata.get("playerName"):
continue # Skip team-level markets
props.append({
"player": pdata["playerName"],
"market": mname,
"line": next(
(m["handicap"] for m in catalog if m["marketId"] == mid),
0
),
"outcome": oname,
"price": pdata["price"],
"book": bk_slug,
"active": pdata.get("active", True),
})
print(f"\nFound {len(props)} player prop entries")
print(f"Players: {len(set(p['player'] for p in props))}")
print(f"Bookmakers: {sorted(set(p['book'] for p in props))}")
print(f"Prop types: {len(set(p['market'] for p in props))}")
# Print a sample
print(f"\n{'Player':<22} {'Market':<30} {'Line':>6} {'Side':<8} {'Price':>6} {'Book'}")
print("-" * 100)
for p in sorted(props, key=lambda x: (x["player"], x["market"]))[:15]:
print(f"{p['player']:<22} {p['market']:<30} {p['line']:>6} {p['outcome']:<8} {p['price']:>6} {p['book']}")
Step 5 — Line Shopping Across Bookmakers
The real value of 350+ bookmakers: price divergence. The same player prop at different books means different edges. Here’s how to find the best price for any prop:
# Group props by player + market + line + outcome for cross-book comparison
from collections import defaultdict
comparisons = defaultdict(list)
for p in props:
key = (p["player"], p["market"], p["line"], p["outcome"])
comparisons[key].append((p["book"], p["price"]))
# Show props available at 2+ bookmakers (line shopping opportunities)
print("=== Line Shopping Opportunities ===\n")
for (player, market, line, outcome), books in sorted(comparisons.items()):
if len(books) < 2:
continue
books_sorted = sorted(books, key=lambda x: -x[1]) # Best price first
best_book, best_price = books_sorted[0]
worst_book, worst_price = books_sorted[-1]
edge = ((best_price - worst_price) / worst_price) * 100
print(f"{player} — {market} ({line}) {outcome}")
for book, price in books_sorted:
marker = " ← BEST" if book == best_book else ""
print(f" {book:<16} {price:.3f}{marker}")
print(f" Edge: {edge:.1f}%\n")
When you're pulling from 4+ bookmakers, you'll regularly find 5-10% price differences on the same player prop. That's the difference between a losing model and a profitable one. For a deeper dive into building automated scanners, see our arbitrage betting bot tutorial and value betting scanner guide.
Combo Props: PRA, Double-Double, Triple-Double
Most APIs either don't support combo props or force you to reconstruct them from individual lines. OddsPapi handles them as native markets with their own market IDs — because that's how bookmakers actually price them.
# Filter the catalog for combo prop markets
combo_keywords = ["Points + Assists + Rebounds", "Points + Assists",
"Points + Rebounds", "Assists + Rebounds",
"Steals + Blocks", "Double", "Triple"]
combo_markets = [
m for m in prop_markets
if any(kw in m["marketName"] for kw in combo_keywords)
]
combo_types = sorted(set(m["marketName"] for m in combo_markets))
print(f"Combo prop types available ({len(combo_types)}):")
for name in combo_types:
lines = [m["handicap"] for m in combo_markets if m["marketName"] == name]
print(f" {name} ({len(lines)} lines: {min(lines)}-{max(lines)})")
# Show combo props from our live odds
combo_props = [p for p in props if any(kw in p["market"] for kw in combo_keywords)]
print(f"\nLive combo props found: {len(combo_props)}")
for p in combo_props:
print(f" {p['player']} — {p['market']} ({p['line']}) {p['outcome']}: {p['price']} @ {p['book']}")
Points + Assists + Rebounds (PRA) is a single market with a single line — not three separate bets multiplied together. Double-Double and Triple-Double are binary yes/no markets. None of the competitor blog posts even mention combo props exist.
Historical Player Prop Odds
The /historical-odds endpoint works for player prop markets too. You can pull the full price history for any prop — see how the line moved from open to close, and backtest your models against closing prices.
# Fetch historical odds for a fixture (max 3 bookmakers per call)
historical = api_get("historical-odds",
fixtureId=fid,
bookmakers="hardrockbet,fanduel,thescore")
# The historical endpoint uses "bookmakers" (not "bookmakerOdds")
# and players["0"] is a LIST of snapshots, not a dict
for bk_slug, bk_data in historical.get("bookmakers", {}).items():
for mid_str, mdata in bk_data.get("markets", {}).items():
mid = int(mid_str)
mname = market_names.get(mid, "Unknown")
if "Player" not in mname:
continue
for oid_str, odata in mdata.get("outcomes", {}).items():
for player_id, snapshots in odata.get("players", {}).items():
if not isinstance(snapshots, list) or not snapshots:
continue
# Each snapshot has: price, createdAt, limit, active, exchangeMeta
player_name = snapshots[0].get("playerName", player_id)
print(f"\n{player_name} — {mname} @ {bk_slug}")
print(f" {len(snapshots)} price snapshots:")
for snap in snapshots[:5]:
print(f" {snap['createdAt'][:19]} → {snap['price']}")
Competitors charge $79-299+/month for historical data. OddsPapi includes it on the free tier. Backtest your player prop model against Pinnacle closing lines without paying a cent. For a full backtesting workflow, see our historical odds backtesting tutorial.
Which Bookmakers Actually Carry Props?
Let's be honest: not all 350+ bookmakers offer player props. Prop coverage varies by sport, fixture, and timing. Here's what we've found in practice:
| Bookmaker | NBA Props | Prop Types | Notes |
|---|---|---|---|
| Hard Rock Bet | ✅ Deep | Points, Rebounds, Assists, PRA, 3PT, Steals, Blocks, Combos, First Point | Deepest prop coverage in testing |
| FanDuel | ✅ Strong | Points, Rebounds, Assists, 3PT, Double-Double, Triple-Double (alt lines: 10+, 15+, 20+...) | Best for alt/milestone lines |
| theScore | ✅ Good | Points, Rebounds, Assists, 3PT (O/U with multiple lines) | Wide line spread per stat |
| BetRivers | ✅ Moderate | Points, Rebounds, Assists, 3PT, PRA (milestone format) | Solid for milestone props |
| Caesars | ✅ Moderate | Points, Rebounds, Assists, spreads | Prop coverage varies by game |
| Pinnacle | ❗ Limited | Mainline markets primarily | When Pinnacle prices a prop, it's the sharpest line available |
| DraftKings | ❗ Limited | Mainline markets primarily | Props may appear closer to game time |
NFL props are the richest during the season — we've verified Anytime TD, First TD, and stat props from FanDuel, Caesars, and Hard Rock Bet during live games. MLB props (strikeouts, hits, runs) are available but thinner. Check our NBA Odds API, NFL Odds API, and MLB Odds API guides for sport-specific deep dives.
The takeaway: use soft books (FanDuel, Hard Rock, BetRivers) for prop coverage, and sharp books (Pinnacle) for prop pricing benchmarks. That's the combo that makes 350+ bookmakers worth it.
Frequently Asked Questions
What is a player props API?
A player props API delivers real-time odds for individual player performance markets — points scored, yards thrown, strikeouts pitched — from multiple bookmakers via a single endpoint. Instead of scraping each sportsbook individually, you make one API call and get structured JSON with prices from every book that offers the prop.
Which sports have player props on OddsPapi?
NBA, NFL, and MLB have the deepest player prop coverage. The market catalog also includes prop types for NHL (goals, saves, shots on goal), soccer (shots, tackles, goals), and cricket (runs, wickets). Prop availability depends on the sport, league, and how close the fixture is to game time.
Can I get historical player prop odds?
Yes. The /historical-odds endpoint returns the full price history for any market ID, including player props. You get timestamped snapshots showing how the line moved from open to close. Historical data is included on the free tier — no upgrade required.
What are combo props (PRA)?
Combo props combine multiple stats into a single market. "Points + Assists + Rebounds" (PRA) Over 29.5 is one bet on a player's combined stat line. OddsPapi handles these as native markets with unique market IDs — they're real bookmaker-priced lines, not reconstructed from individual props.
How many bookmakers offer player props?
It varies by sport and fixture. For NBA, we've consistently seen 4-6 bookmakers returning player props per game (Hard Rock Bet, FanDuel, theScore, BetRivers, and Caesars lead). The total bookmaker count (350+) refers to OddsPapi's full catalog across all market types — player prop coverage is a subset concentrated among US retail sportsbooks.
Start Building with Player Prop Data
Stop guessing which books have the best player prop lines. Query 350+ bookmakers in one API call, parse native combo props, shop lines across books, and backtest your prop models with free historical data.