NBA Odds API: Player Props, Spreads & Moneylines

NBA Odds API - OddsPapi API Blog
How To Guides March 19, 2026

Why You Need an NBA Odds API

The NBA betting market moves fast. Lines shift within seconds of injury reports. Player props are the fastest-growing market in US sports betting. If you’re building models, scanners, or dashboards — you need more than DraftKings and FanDuel. You need the sharp line from Pinnacle, you need 117 bookmakers, and you need 1,154 markets per game.

Most APIs give you moneylines from 20 soft books. OddsPapi gives you moneylines, spreads, totals, player props, halves, and quarters from 117+ bookmakers — including the sharps that set the market. Pinnacle, Singbet, SBOBet. The books that move first.

One API call. 117 bookmakers. 1,154 markets. Every NBA game. Let’s build.

NBA Coverage: OddsPapi vs The Competition

Feature The Odds API SportsGameOdds OddsPapi
Bookmakers per game ~15-20 ~30 117+
Moneylines Yes Yes Yes (111 books)
Spreads Basic Yes Every line (76-93 books)
Game Totals Basic Yes Every line (82-95 books)
Player Props No Limited Yes (full depth)
Half/Quarter lines No Limited Yes (1H, 1Q, team totals)
Sharp books (Pinnacle) No No Yes
DraftKings/FanDuel Yes Yes Yes
Free tier 500 req/mo Limited 250 req/mo

OddsPapi also includes free historical odds data on the free tier — backtest your NBA models without paying a cent. Competitors charge extra for this. And when you need real-time updates, WebSocket streaming pushes line changes to your app the instant they happen. No polling. No stale data.

1,154 Markets Per Game: What’s Covered

Every NBA fixture on OddsPapi comes loaded with markets. Here’s what you’re working with:

Market Type Market ID(s) Books Notes
Moneyline 111 111+ Stable ID. Home/Away.
Game Spread 113xx-114xx 76-93 Each line gets unique ID. Parse bookmakerOutcomeId for line value.
Game Total (O/U) 112xx 82-95 Each line gets unique ID. Parse bookmakerOutcomeId for line.
1st Half Moneyline 11344 ~60 Home/Away for first half.
1st Quarter Moneyline 11350 ~50 Home/Away for first quarter.
1st Half Spread 118xxx ~40 Dynamic per line.
Team Totals 111xxx ~30 Home and away team totals separate.
Player Props 111xxx 10-30 Each player/prop combo is a unique market.

How Spread & Total Market IDs Work

Unlike soccer’s fixed market IDs (e.g., 101 for Full Time Result), NBA spreads and totals are dynamic — each line gets its own market ID per fixture. The bookmakerOutcomeId field tells you the actual line: -7.5/home means home team -7.5, 228.5/over means game total over 228.5.

This makes it easy to compare the same spread across 90+ bookmakers programmatically. No guessing which market is which — the bookmakerOutcomeId is your decoder ring.

Python Tutorial: NBA Odds in 5 Steps

Step 1: Setup

import requests
from datetime import datetime, timedelta

API_KEY = "YOUR_API_KEY"  # Free at oddspapi.io
BASE_URL = "https://api.oddspapi.io/v4"
SPORT_ID = 11  # Basketball

Authentication is simple: pass your API key as a query parameter. No headers, no OAuth, no tokens. Get a free API key here.

Step 2: Get Tonight’s NBA Games

def get_nba_fixtures():
    """Get today's NBA fixtures with odds."""
    today = datetime.now().strftime("%Y-%m-%d")
    tomorrow = (datetime.now() + timedelta(days=1)).strftime("%Y-%m-%d")

    response = requests.get(f"{BASE_URL}/fixtures", params={
        "apiKey": API_KEY,
        "sportId": SPORT_ID,
        "from": today,
        "to": tomorrow
    })

    fixtures = response.json()
    nba = [f for f in fixtures if "NBA" in f.get("tournamentName", "") and f.get("hasOdds")]
    print(f"{len(nba)} NBA games tonight")
    return nba

games = get_nba_fixtures()
for g in games:
    print(f"  {g['participant1Name']} vs {g['participant2Name']}")

The fixtures endpoint returns a flat list. Filter by tournamentName to isolate NBA games. The hasOdds flag tells you which fixtures have pricing data available. OddsPapi covers 462 basketball tournaments globally — NBA, NCAA, EuroLeague, WNBA, CBA, and more.

Step 3: Fetch Odds from 117+ Bookmakers

def get_odds(fixture_id):
    """Get odds from all bookmakers for a game."""
    response = requests.get(f"{BASE_URL}/odds", params={
        "apiKey": API_KEY,
        "fixtureId": fixture_id
    })
    return response.json()

game = games[0]
odds = get_odds(game["fixtureId"])
bookmakers = list(odds.get("bookmakerOdds", {}).keys())
print(f"{len(bookmakers)} bookmakers pricing {game['participant1Name']} vs {game['participant2Name']}")

One call to /odds returns every bookmaker, every market, every outcome. The response is a flat dict with bookmakerOdds at the top level — no pagination, no nested wrappers.

Step 4: Compare Moneylines

def compare_moneylines(odds_data):
    """Compare moneyline (market 111) across all bookmakers."""
    results = []

    for slug, bookie in odds_data.get("bookmakerOdds", {}).items():
        market = bookie.get("markets", {}).get("111")
        if not market:
            continue

        row = {"bookmaker": slug}
        for outcome_id, outcome in market.get("outcomes", {}).items():
            for player_id, player in outcome.get("players", {}).items():
                boid = player.get("bookmakerOutcomeId", "")
                price = player.get("price")
                if "home" in str(boid).lower() or outcome_id == list(market["outcomes"].keys())[0]:
                    row["home"] = price
                else:
                    row["away"] = price

        if "home" in row and "away" in row:
            results.append(row)

    results.sort(key=lambda x: x.get("home", 0), reverse=True)

    print(f"\nMoneylines from {len(results)} bookmakers:")
    print(f"{'Bookmaker':<20} {'Home':>8} {'Away':>8}")
    print("-" * 38)
    for r in results[:10]:
        print(f"{r['bookmaker']:<20} {r.get('home', 'N/A'):>8} {r.get('away', 'N/A'):>8}")

    best_home = max(results, key=lambda x: x.get("home", 0))
    best_away = max(results, key=lambda x: x.get("away", 0))
    print(f"\nBest Home: {best_home['home']} @ {best_home['bookmaker']}")
    print(f"Best Away: {best_away['away']} @ {best_away['bookmaker']}")

compare_moneylines(odds)

Market 111 is the moneyline — the only stable, universal market ID for NBA. Every bookmaker prices it. This function finds the best home and away odds across 111+ books in one pass.

Step 5: Parse Spreads & Totals

def find_spreads(odds_data, target_line=None):
    """Find spread markets and compare across bookmakers.

    NBA spreads use dynamic market IDs. The bookmakerOutcomeId
    contains the line (e.g., '-7.5/home').
    """
    spreads = {}  # {line: [{bookmaker, home_price, away_price}]}

    for slug, bookie in odds_data.get("bookmakerOdds", {}).items():
        for mid, market in bookie.get("markets", {}).items():
            for oid, outcome in market.get("outcomes", {}).items():
                for pid, player in outcome.get("players", {}).items():
                    boid = str(player.get("bookmakerOutcomeId", ""))
                    price = player.get("price")

                    # Spread boids look like "-7.5/home" or "-7.5/away"
                    if "/" in boid and ("home" in boid or "away" in boid):
                        parts = boid.split("/")
                        try:
                            line = float(parts[0])
                            side = parts[1]
                        except (ValueError, IndexError):
                            continue

                        # Filter to spread-like lines (not totals which are 200+)
                        if abs(line) > 50:
                            continue

                        if line not in spreads:
                            spreads[line] = {}
                        if slug not in spreads[line]:
                            spreads[line][slug] = {}
                        spreads[line][slug][side] = price

    # Show available lines
    print(f"Available spread lines: {sorted(spreads.keys())}")

    # Compare a specific line
    if target_line and target_line in spreads:
        line_data = spreads[target_line]
        print(f"\nSpread {target_line} from {len(line_data)} bookmakers:")
        print(f"{'Bookmaker':<20} {'Home':>8} {'Away':>8}")
        print("-" * 38)
        for slug, prices in sorted(line_data.items(), key=lambda x: x[1].get('home', 0), reverse=True)[:10]:
            print(f"{slug:<20} {prices.get('home', 'N/A'):>8} {prices.get('away', 'N/A'):>8}")

    return spreads

spreads = find_spreads(odds)

The key insight: NBA spreads don’t have a single market ID. Each line (-7.5, -3.5, +1.5, etc.) gets its own unique market ID per fixture. The bookmakerOutcomeId field is your decoder — it contains both the line and the side in a format like -7.5/home.

Accessing NBA Player Props

Player props are where the edge lives in NBA betting. Soft bookmakers are slow to adjust lines after injury news or lineup changes. With 117 bookmakers in one API call, you can spot stale lines in seconds.

Player props use high market IDs (111xxx range). Each player/prop combination — LeBron points over 25.5, Curry assists over 6.5 — gets its own market ID. The playerName field identifies the player.

def find_player_props(odds_data):
    """Find player prop markets. Props have playerName set."""
    props = []

    for slug, bookie in odds_data.get("bookmakerOdds", {}).items():
        for mid, market in bookie.get("markets", {}).items():
            for oid, outcome in market.get("outcomes", {}).items():
                for pid, player in outcome.get("players", {}).items():
                    name = player.get("playerName")
                    if name:  # Player props have playerName set
                        props.append({
                            "market_id": mid,
                            "player": name,
                            "price": player.get("price"),
                            "bookmaker": slug,
                            "line": player.get("bookmakerOutcomeId", "")
                        })

    # Group by player
    from collections import defaultdict
    by_player = defaultdict(list)
    for p in props:
        by_player[p["player"]].append(p)

    print(f"Player props found: {len(props)} across {len(by_player)} players")
    for player, entries in sorted(by_player.items()):
        books = len(set(e["bookmaker"] for e in entries))
        markets = len(set(e["market_id"] for e in entries))
        print(f"  {player}: {markets} prop markets from {books} bookmakers")

find_player_props(odds)

This is the data that sharp bettors use to find value. When DraftKings has Jokic over 25.5 points at -110 and Pinnacle has it at -105, someone is wrong. OddsPapi lets you see both in one response.

DraftKings vs Pinnacle: Where the Lines Diverge

DraftKings and FanDuel set the lines that 90% of the US market sees. Pinnacle sets the line that sharps respect. When these two diverge, there’s opportunity.

def compare_books(odds_data, book1="pinnacle", book2="draftkings"):
    """Compare two bookmakers across all shared markets."""
    b1 = odds_data.get("bookmakerOdds", {}).get(book1, {}).get("markets", {})
    b2 = odds_data.get("bookmakerOdds", {}).get(book2, {}).get("markets", {})

    shared = set(b1.keys()) & set(b2.keys())
    print(f"{book1} markets: {len(b1)}, {book2} markets: {len(b2)}, shared: {len(shared)}")

    # Compare moneyline
    if "111" in shared:
        print(f"\nMoneyline (market 111):")
        for slug, markets in [(book1, b1), (book2, b2)]:
            m = markets["111"]
            for oid, outcome in m.get("outcomes", {}).items():
                for pid, player in outcome.get("players", {}).items():
                    boid = player.get("bookmakerOutcomeId", oid)
                    print(f"  {slug}: {boid} = {player.get('price')}")

compare_books(odds)

Pinnacle’s margin on NBA moneylines is typically 2-3%. DraftKings runs 4-5%. That difference is where value bettors and arbitrageurs operate. With OddsPapi, you can compare both — plus 115 other books — in a single API call.

FAQ: NBA Odds API

What sport ID is basketball/NBA?

Sport ID 11 covers all basketball. The NBA specifically is tournament ID 132. Use sportId=11 in the fixtures endpoint, then filter by tournamentName to isolate NBA games.

How many bookmakers cover NBA games?

117+ bookmakers per game, including sharp books like Pinnacle, SBOBet, and Singbet, US books like DraftKings and FanDuel, the Betfair Exchange, and crypto books like 1xBet.

Do you have NBA player props?

Yes. Each player/prop combination has its own market ID in the 111xxx range. The playerName field identifies the player, and the bookmakerOutcomeId contains the line and direction.

How do NBA spread market IDs work?

Each spread line gets a unique market ID per fixture. A game might have 50+ spread markets (one for each line like -7.5, -3.5, +1.5, etc.). Parse the bookmakerOutcomeId field to get the actual line — it’ll look like -7.5/home or +7.5/away.

Is the API free for NBA data?

The free tier gives you 250 requests per month. That’s enough to check lines for every NBA game, every night. Historical odds data is also included free — backtest your models without upgrading.

Do you cover college basketball (NCAA)?

Yes. OddsPapi covers 462 basketball tournaments globally, including NCAA, EuroLeague, WNBA, CBA (China), NBB (Brazil), and more. Same market depth, same bookmaker coverage.

Start Building with NBA Data

DraftKings and FanDuel set lines that 90% of bettors see. Pinnacle sets the line that sharps respect. With OddsPapi, you get both — plus 115 more bookmakers — in one API call.

117 bookmakers. 1,154 markets. Player props, spreads, totals, halves, quarters. Every NBA game, every night. Free tier included.

Get Your Free API Key