Skip to the tutorial

Python API tutorial

Fetch NBA player props with Python

Build a comparison-ready prop board from current sportsbook prices without mixing players, periods, sides or alternate lines.

Free key. Official Python client. No credit card.

The hard part is not downloading NBA props. It is proving that two sportsbook prices describe the same bet.

This tutorial finds the current NBA slate, requests points, rebounds and assists, follows every pagination cursor and keeps the best available decimal price for each exact selection. It uses the official odds-api-client package and the normalized fields documented by odds-api.net.

Start with the NBA Odds API page if you also need schedules, scores, results or main game markets. Use the broader Player Props API page for other leagues and the complete prop contract.

The safe comparison key

event + player + market + period + line + side

Do not group by player name alone.

Over 24.5 points and over 25.5 points are different bets. Full-game points and first-quarter points are different bets. A higher price is only better after every bet-defining field matches.

Install the Python client

You need Python 3, an odds-api.net key and one package. The client reads the key from ODDS_API_KEY, so it never needs to appear in source code.

terminalShell
python -m pip install odds-api-client

# macOS or Linux
export ODDS_API_KEY="your_key"

# PowerShell
$env:ODDS_API_KEY="your_key"

Create a free key if you do not have one. The public GitHub repository contains the Python SDK, OpenAPI document and runnable examples.

Run the complete NBA props script

Save this as nba_props.py. It finds the next NBA game returned by the API, pages through the selected prop markets and prints the best currently available price for each stable selection.

nba_props.pyPython
import time
from odds_api import OddsApiClient

PROP_MARKETS = [
    "player points",
    "player rebounds",
    "player assists",
]

client = OddsApiClient()


def fetch_all_props(event_id):
    rows = []
    cursor = None
    newest_as_of_ms = 0
    shortest_ttl_seconds = None

    while True:
        page = client.get_odds_snapshot(
            event_id,
            types="player prop",
            market_keys=PROP_MARKETS,
            price_fields="odds,fair",
            include_unavailable=True,
            limit=2_000,
            cursor=cursor,
        )
        rows.extend(page.get("items", []))
        newest_as_of_ms = max(
            newest_as_of_ms,
            page.get("as_of_ts_ms") or 0,
        )
        ttl = page.get("ttl_seconds")
        if ttl is not None:
            shortest_ttl_seconds = (
                ttl if shortest_ttl_seconds is None
                else min(shortest_ttl_seconds, ttl)
            )

        cursor = page.get("next_cursor")
        if not cursor:
            return rows, newest_as_of_ms, shortest_ttl_seconds


events = client.search_events(
    sport="basketball",
    league="NBA",
    not_started_only=True,
    limit=25,
)

if not events.get("items"):
    raise SystemExit(
        "No upcoming NBA events are available in this response. "
        "Try again when an NBA slate is posted."
    )

event = events["items"][0]
event_id = event["event_id"]
rows, as_of_ms, ttl_seconds = fetch_all_props(event_id)

if ttl_seconds and as_of_ms:
    age_seconds = max(0, time.time() - as_of_ms / 1_000)
    if age_seconds > ttl_seconds:
        raise SystemExit(
            f"Snapshot is {age_seconds:.0f}s old; refresh before comparing."
        )

best = {}
for row in rows:
    odds = row.get("odds")
    if row.get("is_available") is False:
        continue
    if not isinstance(odds, (int, float)):
        continue

    # selection_key is preferred because it stays stable across price changes.
    key = row.get("selection_key") or (
        row.get("event_id", event_id),
        row.get("player_name"),
        row.get("market_key"),
        row.get("period_str") or row.get("period"),
        row.get("line"),
        row.get("side"),
    )

    if key not in best or odds > best[key]["odds"]:
        best[key] = row

if not best:
    raise SystemExit(
        "The game exists, but the selected props are not currently posted."
    )

for row in sorted(
    best.values(),
    key=lambda item: (
        item.get("player_name") or "",
        item.get("market_key") or "",
        str(item.get("line") or ""),
        item.get("side") or "",
    ),
):
    print(
        row.get("player_name"),
        row.get("market_key"),
        row.get("line"),
        row.get("side"),
        row.get("bookmaker"),
        row.get("odds"),
    )

The output contains one best sportsbook price per exact selection. It is a comparison feed, not a player projection or a betting recommendation.

Why selection_key matters

A prop row contains readable fields such as player_name, market_key, line and side. It can also contain a stable selection_key. The SDK itself prefers that key when finding best odds because it prevents a price change from creating a new identity.

FieldWhy retain itBad comparison it prevents
event_idCanonical game identityMixing two Lakers games
player_namePlayer attached to the marketComparing two different players
market_keyNormalized statisticPoints against points + rebounds
period_strFull game, half or quarterFull-game points against first-quarter points
lineThe numerical thresholdOver 24.5 against over 25.5
sideOver, under or another outcomeOver against under
selection_keyStable selection identityLosing continuity when the price moves

If selection_key is absent in an older or reduced response, the tuple used in the script is the safe fallback. Never shorten it to player + market.

An empty response is not automatically an error

NBA exists in the league catalogue all year. Player-prop inventory does not. The current event response can be empty during the offseason, before sportsbooks post a slate, when a particular market is not offered or after a line is removed.

No events

No NBA game matched the schedule filters. Widen the time window or wait for the next slate.

Event, no props

The game exists, but the requested prop markets have not been posted or are not covered.

Unavailable row

The selection exists but is suspended or otherwise unavailable. Do not carry its old price forward.

Missing fair odds

fair_odds is nullable. Keep the sportsbook price and treat fair price as unavailable—not zero.

This distinction is useful in production. A retry may help when markets are not posted yet; changing code will not create a slate that does not exist.

NBA player-prop market keys

Request a small explicit list first. The current game response remains the source of truth because prop inventory changes by sportsbook, player and time.

GroupNormalized market keys
Scoringplayer points, player threes, player field goals
Box-score statsplayer assists, player rebounds, player blocks, player steals, player turnovers
Combined statsplayer pra, player pr, player pa, player ra
Pick'empickem player points

Use the player-prop market catalogue when you need the wider normalized list.

Reject stale, missing and suspended prices

Three fields make the snapshot usable in a real application: as_of_ts_ms records when the response state was assembled, ttl_seconds gives its cache policy and is_available says whether a line should still be treated as available.

  1. 01
    Check availability first

    Discard rows where is_available is false. Never reuse yesterday's price just because the selection identity still exists.

  2. 02
    Require a numeric price

    A missing odds value is not a zero price and should not enter comparison logic.

  3. 03
    Display snapshot age

    Calculate age from as_of_ts_ms. Respect ttl_seconds and refresh instead of silently showing expired state.

  4. 04
    Persist the resume token

    If you move to SSE or WebSocket updates, start from the snapshot's resume value and reload the snapshot after a resync event.

The reliable lifecycle is snapshot → resume token → stream deltas → resync when instructed. Streaming without the initial snapshot cannot reconstruct lines that never changed after your connection opened.

Query NBA prop line movement

Once a row has a selection_key, use it to request bounded history for that exact selection. Pass ISO 8601 UTC timestamps and choose the price series you want.

line_movement.pyPython
from datetime import datetime, timedelta, timezone

selection = next(
    row for row in rows
    if row.get("selection_key") and row.get("is_available", True)
)

to_time = datetime.now(timezone.utc)
from_time = to_time - timedelta(hours=6)

movement = client.get_line_movement(
    event_id,
    selection["selection_key"],
    from_ts=from_time.isoformat(),
    to_ts=to_time.isoformat(),
    price_type="odds",
    limit_points_per_bookmaker=500,
)

for series in movement.get("series", []):
    print(series.get("bookmaker_name"), series.get("points", []))

Use price_type="odds" for quoted sportsbook movement, odds_no_vig for the available no-vig series or fair_odds for the available fair-price series. Keep the time window and point limit bounded.

History is analysis, not recovery.

Use the live snapshot as current state. Historical ticks are for charts, research and line-movement analysis; do not use them to rebuild a missing live board.

Production checklist

  • Keep the API key in ODDS_API_KEY, never in the script or browser.
  • Discover games first and carry the canonical event_id into every odds request.
  • Keep snapshot filters identical while following next_cursor.
  • Compare only the same selection key or the full fallback tuple.
  • Retain alternate lines as separate selections.
  • Reject unavailable, non-numeric and expired prices.
  • Handle HTTP 429 with backoff instead of tight retry loops.
  • Treat an empty offseason or pre-posting slate as a valid state.
  • Use resume and reload after resync when streaming.
  • Do not present odds comparison as a player projection or guaranteed edge.

NBA player props API Python FAQ

Can I get NBA player props with Python?

Yes. Use the official odds-api-client package to find a current NBA event, then request its odds snapshot with types=player prop and the player market keys you need.

Why does the NBA props response sometimes contain no items?

An empty response can be valid when no NBA game is in the requested window, sportsbooks have not posted those props yet, the slate is out of season or the selected market is unavailable.

How should I compare NBA prop odds across sportsbooks?

Compare only rows with the same event, player, market, period, line and side. Prefer the stable selection_key returned by the API, and reject unavailable or non-numeric prices.

Does the API include alternate NBA player-prop lines?

Alternate lines can appear when supplied for the current event. Treat every line value as a different selection and use the current event response as the source of truth.

Can I query historical NBA player-prop movement?

Yes. Keep the selection_key from a current snapshot, then pass it to the bounded odds history endpoint with ISO 8601 from_ts and to_ts values. History is for line-movement analysis, not live-state recovery.

Run it on the next NBA slate

Create a free key, fetch one current game and verify that each displayed comparison preserves the player, market, period, line and side.