Player Props API: How to Get NFL, NBA & MLB Prop Odds in Python

Player Props API - OddsPapi API Blog
How To Guides May 12, 2026

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:

  1. The playerName field in the outcome is non-null (e.g., “Cunningham, Cade”)
  2. The players dict 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())}")
⚠️ Not all bookmakers carry player props. In our testing, Hard Rock Bet, FanDuel, theScore, and BetRivers consistently return NBA player props. Pinnacle doesn’t offer as many player prop markets as US soft books — but when Pinnacle does price a prop, that line is the sharpest benchmark you have. Only fixtures with 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.

Get your free API key at oddspapi.io →