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.
event + player + market + period + line + side
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.
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.
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.
| Field | Why retain it | Bad comparison it prevents |
|---|---|---|
event_id | Canonical game identity | Mixing two Lakers games |
player_name | Player attached to the market | Comparing two different players |
market_key | Normalized statistic | Points against points + rebounds |
period_str | Full game, half or quarter | Full-game points against first-quarter points |
line | The numerical threshold | Over 24.5 against over 25.5 |
side | Over, under or another outcome | Over against under |
selection_key | Stable selection identity | Losing 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 NBA game matched the schedule filters. Widen the time window or wait for the next slate.
The game exists, but the requested prop markets have not been posted or are not covered.
The selection exists but is suspended or otherwise unavailable. Do not carry its old price forward.
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.
| Group | Normalized market keys |
|---|---|
| Scoring | player points, player threes, player field goals |
| Box-score stats | player assists, player rebounds, player blocks, player steals, player turnovers |
| Combined stats | player pra, player pr, player pa, player ra |
| Pick'em | pickem 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.
- 01Check availability first
Discard rows where
is_availableis false. Never reuse yesterday's price just because the selection identity still exists. - 02Require a numeric price
A missing
oddsvalue is not a zero price and should not enter comparison logic. - 03Display snapshot age
Calculate age from
as_of_ts_ms. Respectttl_secondsand refresh instead of silently showing expired state. - 04Persist the resume token
If you move to SSE or WebSocket updates, start from the snapshot's
resumevalue and reload the snapshot after aresyncevent.
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.
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.
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_idinto 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
resumeand reload afterresyncwhen 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.