Find NBL games
Request NBL events and keep each event_id for follow-up calls.
Quick start with the NBL API.
Discover Australian and New Zealand NBL games, load game lines and player markets, then separate prices by period and selection. Use the same event ID for game records, price history and streams.
Request NBL events and keep each event_id for follow-up calls.
Page through the game snapshot to collect every available market.
Apply odds deltas and read the result when the game finishes.
curl -sS -G 'https://api.odds-api.net/v1/events' \
-H "X-API-Key: $ODDS_API_KEY" \
--data-urlencode 'sport=basketball' \
--data-urlencode 'league=NBL'
X-API-Key: $ODDS_API_KEY
League filtersport=basketball&league=NBL
Endpoints
The public API separates match discovery, match records, odds, and results. A stream sends changes after an initial snapshot.
| Need | Endpoint | What it returns |
|---|---|---|
| Current coverage | GET /v1/coverage?sport=basketball&league=NBL | NBL league, bookmaker, and approximate market coverage. |
| Fixtures | GET /v1/events?league=NBL | Teams, scheduled tip-off time, canonical IDs, and pages of matches. |
| Match record | GET /v1/events/{event_id} | The source match record under data. |
| Match result | GET /v1/events/{event_id}/results | pending or available, with the result record when present. |
| All match odds | GET /v1/events/{event_id}/odds/snapshot | Current rows across available bookmakers, markets, and periods. Follow every next_cursor. |
| All match odds changes | GET /v1/events/{event_id}/odds/stream | SSE changes for that match's odds. WebSocket: /odds/ws. |
| Line movement | GET /v1/events/{event_id}/odds/history | Retained points for one selection_key and time range when history access is enabled. |
| NBL main lines | GET /v1/odds/main-lines/stream?leagues=NBL | A league-wide feed of main lines. It is a smaller view than every match market. |
Coverage
Request GET /v1/coverage?sport=basketball&league=NBL to build your league and bookmaker filters. The coverage response contains bookmakers, leagues, and approximate markets with an as_of timestamp. Use the canonical keys returned by GET /v1/bookmakers when filtering odds.
curl -sS -G 'https://api.odds-api.net/v1/coverage' \
--data-urlencode 'sport=basketball' \
--data-urlencode 'league=NBL'
The bookmakers field on a NBL event shows which bookmakers are attached to that match. The odds snapshot shows their current selections. For bookmaker-specific context, see Sportsbet, TAB, Ladbrokes, and Bet365.
Events and results
Page /v1/events with sport=basketball and league=NBL. Set start_from and start_to as Unix seconds when you need live matches as well as upcoming ones. Without start_from, the endpoint starts at the current time.
curl -sS -G 'https://api.odds-api.net/v1/events' \
-H "X-API-Key: $ODDS_API_KEY" \
--data-urlencode 'sport=basketball' \
--data-urlencode 'league=NBL' \
--data-urlencode 'limit=200'
Illustrative response. Use the returned IDs and times; these values are examples.
{
"count": 1,
"items": [
{
"away_team": "Sydney Kings",
"bookmakers": {
"sportsbet": null,
"tab": null
},
"event_id": "nbl-example-001",
"home_team": "Tasmania JackJumpers",
"league": "NBL",
"sport": "basketball",
"start_time": 1788472800
}
],
"next_cursor": null
}
Pass next_cursor back as cursor with the same time filters until it is null. Use event_id for every follow-up request.
GET /v1/events/{event_id}The response contains event_id and a data object. Read the fields actually supplied for that match; the record can carry more source detail than the event list.
GET /v1/events/{event_id}/resultsThe result endpoint returns status as pending or available. An available response contains the result record. Handle pending and available explicitly, and read score fields from the returned source result record.
A match without a result record returns:
{
"event_id": "nbl-example-001",
"result": null,
"status": "pending"
}
Odds snapshots
Leave bookmakers, types, market_keys, and periods out of the request. That returns all odds rows available to your account for the chosen match. price_fields=all includes the supported price fields; include_unavailable=true also returns suspended rows.
curl -sS -G "https://api.odds-api.net/v1/events/$EVENT_ID/odds/snapshot" \
-H "X-API-Key: $ODDS_API_KEY" \
--data-urlencode 'limit=10000' \
--data-urlencode 'price_fields=all' \
--data-urlencode 'include_unavailable=true' \
--data-urlencode 'include_source=true'
Continue with the snapshot's next_cursor until it is null. Save the resume token from the first page before opening the stream, so changes during later pages can be replayed. Match stream changes by the odds-row id.
Illustrative snapshot with match odds and a player points prop. API prices are decimal.
{
"as_of_ts_ms": 1788472500000,
"event_id": "nbl-example-001",
"items": [
{
"bet_type": "moneyline",
"bookmaker": "sportsbet",
"event_id": "nbl-example-001",
"fair_odds": 1.95,
"id": "line-example-001",
"is_available": true,
"market_key": "moneyline",
"odds": 1.91,
"odds_no_vig": 1.96,
"period": "full game",
"selection_key": "selection-example-home",
"selection_name": "Tasmania JackJumpers",
"side": "home"
},
{
"bet_type": "player prop",
"bookmaker": "sportsbet",
"event_id": "nbl-example-001",
"id": "line-example-002",
"is_available": true,
"line": "19.5",
"market_key": "player points",
"odds": 2.05,
"period": "full game",
"player_name": "Example Player",
"selection_key": "selection-example-prop-over",
"side": "over"
}
],
"next_cursor": null,
"resume": "1788472500000-0",
"ttl_seconds": 120
}
Markets
market_key identifies the normalized market in an odds row. Use market_keys in a query only when your product needs a subset. Keep period, line, side, and player_name when comparing two prices.
| Market | API market keys | Meaning |
|---|---|---|
| Head-to-head | moneyline |
Two-way game-winner prices for the home and away teams. |
| Line betting | handicap |
Point-start lines, including alternate thresholds and period-specific prices. |
| Game totals | total |
Over and under prices for combined points, with game, half and quarter periods. |
| Team totals | team total |
Over and under prices for one team's points. |
| Player scoring | player pointsplayer threes |
Player points and made-three-pointer lines, matched by player and threshold. |
| Rebounds and assists | player reboundsplayer assists |
Player rebound and assist thresholds with an over or under side. |
| Three-way result | moneyline 3w |
Three-way result prices include a draw selection. Keep them separate from two-way head-to-head prices. |
Use handicap for line betting, total for combined points, and literal player points, player rebounds, player assists and player threes keys for props. Separate each game period and threshold. Keep the period, line, player, and side when comparing exact prop selections across bookmakers.
Basketball market matching
A game line, first-half line and first-quarter line are three different bets. Keep the market's period alongside its team, line and side.
Load the selected NBL game's snapshot without a periods filter. Follow every next_cursor, then use the returned period labels to separate game, half and quarter markets.
curl -sS -G "https://api.odds-api.net/v1/events/$EVENT_ID/odds/snapshot" \
-H "X-API-Key: $ODDS_API_KEY" \
--data-urlencode 'bookmakers=sportsbet,tab,ladbrokes' \
--data-urlencode 'price_fields=all'
# rows contains items collected from every snapshot page.
def period_label(row):
return str(row.get("period_str") or row.get("period") or "").strip().lower()
first_quarter = [row for row in rows if period_label(row) == "1st quarter"]
first_half = [row for row in rows if period_label(row) == "1st half"]
Use period_str when present; compact responses put the display label in period. Keep the label with the market key, team, line and side when matching prices. A numeric period alone can be shared by a half and a quarter. The server's periods filter matches the stored period value, so do not substitute a display label for a numeric value.
moneyline is a two-way market. moneyline 3w includes a draw selection. Match the period and market rules before comparing prices; a regulation-time result and an overtime-inclusive game winner are different bets.
Points, rebounds, assists and made threes use distinct market keys. Group by player_name, then keep the period label, line and side on every row.
curl -sS -G "https://api.odds-api.net/v1/events/$EVENT_ID/odds/snapshot" \
-H "X-API-Key: $ODDS_API_KEY" \
--data-urlencode 'market_keys=player points,player rebounds,player assists,player threes' \
--data-urlencode 'price_fields=all' \
--data-urlencode 'include_unavailable=true'
A player points line of 19.5 is a betting threshold. Use the returned odds and availability state to display the offered bet; do not interpret the line as the player's recorded score.
Read bookmaker_as_of_ts_ms for the bookmaker being displayed. The top-level snapshot timestamp can advance when a different bookmaker updates, so it does not establish the age of every quote.
Historical data
Build a price chart for one NBL game line, total or player selection. Keep the game period and exact threshold fixed as you compare timestamped bookmaker quotes.
History access is required. History Lite includes 30 days of odds history; your account's history access determines the query window.
Keep the event_id and selection_key from a match odds snapshot. Copy the selection key exactly. Set FROM_TS and TO_TS to ISO 8601 UTC timestamps within your history window, then run:
curl -sS -G "https://api.odds-api.net/v1/events/$EVENT_ID/odds/history" \
-H "X-API-Key: $ODDS_API_KEY" \
--data-urlencode "selection_key=$SELECTION_KEY" \
--data-urlencode "from_ts=$FROM_TS" \
--data-urlencode "to_ts=$TO_TS" \
--data-urlencode 'bookmakers=sportsbet,tab' \
--data-urlencode 'price_type=odds' \
--data-urlencode 'price_fields=all' \
--data-urlencode 'include_unavailable=true' \
--data-urlencode 'limit_points_per_bookmaker=2000' \
--output nbl-history.json
series contains one entry per bookmaker. Each point carries tick_ts, is_available and its price fields. Use the timestamp for the chart's horizontal axis and the price for its vertical axis.
{
"bookmaker_name": "sportsbet",
"points": [
{"tick_ts": "2026-10-01T06:00:00Z", "is_available": true, "odds": 2.2},
{"tick_ts": "2026-10-01T06:15:00Z", "is_available": true, "odds": 2.1},
{"tick_ts": "2026-10-01T06:30:00Z", "is_available": true, "odds": 2.0}
]
}
Compare the same match, market, period, player, side and line. A JackJumpers −3.5 full-game line and a −3.5 first-quarter line are separate bets. Player points over 19.5 and over 24.5 also require separate series. Keep suspended points as gaps in a chart rather than carrying them forward as offered prices.
odds is the bookmaker quote. odds_no_vig removes the bookmaker margin. fair_odds is the composite fair-price estimate. The response's meta.available_price_types lists its price types.
The first and last points describe your requested time window. For a pre-match closing price, use the last available point at or before tip-off time. A sample limit can shorten a series; narrow the time window when a bookmaker reaches limit_points_per_bookmaker.
After loading historical points, follow that selection through the history SSE stream. Save each stream resume token and pass it as since when reconnecting.
curl -N -G "https://api.odds-api.net/v1/events/$EVENT_ID/odds/history/stream" \
-H "X-API-Key: $ODDS_API_KEY" \
--data-urlencode "selection_key=$SELECTION_KEY" \
--data-urlencode 'bookmakers=sportsbet,tab' \
--data-urlencode 'price_fields=all' \
--data-urlencode 'include_unavailable=true'
Use /v1/events/{event_id}/odds/history/ws for WebSockets with the same filters. A resync message means reload the bounded history query before continuing the chart.
Price analysis
Calculate implied probabilities and bookmaker overround for NBL game markets, then measure how a selected line or player price changes before tip-off.
The API returns odds, odds_no_vig and fair_odds with price_fields=all. Calculate the statistics below in your app from snapshot rows or historical points.
| Statistic | Calculation | Use it for |
|---|---|---|
| Implied probability | 100 / decimal_odds | Expressing a price as a percentage. Odds of 2.00 imply 50%. |
| Bookmaker overround | (sum(1 / odds) - 1) * 100 | Measuring the excess implied probability across every outcome in one bookmaker's market. |
| Proportional no-vig probability | (1 / odds) / sum(1 / odds) * 100 | Normalising a complete market to 100%. See other margin-removal methods. |
| Best price | max(matched_available_odds) | Comparing bookmakers for exactly the same selection at a comparable time. |
| Price movement | (last / first - 1) * 100 | Showing how far the decimal price moved within your chosen time window. |
| Probability movement | 100 / last - 100 / first | Showing the change in implied probability in percentage points. |
| Price range | min(prices), max(prices) | Finding the shortest and longest recorded prices for one bookmaker and selection. |
With 1.75 on Tasmania and 2.25 on Sydney in a two-way market, implied probabilities are 57.14% and 44.44%. The overround is 1.59%; proportional no-vig probabilities are 56.25% and 43.75%.
prices = [1.75, 2.25]
implied = [1 / price for price in prices]
book = sum(implied)
overround_pct = (book - 1) * 100
no_vig_pct = [probability / book * 100 for probability in implied]
Use all outcomes from the same bookmaker, market, line and period. A three-way market includes the draw. Do not mix opposing prices from different bookmakers when measuring one bookmaker's margin.
Download nbl_odds_analysis.py. It reads the history JSON saved above and reports each bookmaker's first, last, minimum and maximum price, sample count, price change and probability change. It sorts by timestamp and excludes suspended or invalid prices.
python nbl_odds_analysis.py nbl-history.json
# Add a market calculation using your own matched prices.
python nbl_odds_analysis.py nbl-history.json --home-odds 1.75 --away-odds 2.25
A player-points price moving from 2.20 to 2.00 shortens by 9.09%. Its implied probability moves from 45.45% to 50.00%, a change of 4.55 percentage points.
The game result endpoint returns pending or available with its source result record. Player points, rebounds and assists odds describe bookmaker selections, not recorded player box scores or play-by-play.
Combine the result record with your stored match and price observations to analyse completed matches. Keep the event ID, selection identity, bookmaker and observation time alongside each calculation.
SSE and WebSocket
Open one odds stream for each NBL match you are tracking. Pass the snapshot's resume token as since, with the same price and availability options used for the snapshot. The stream returns changes, not another full odds list.
curl -N -G "https://api.odds-api.net/v1/events/$EVENT_ID/odds/stream" \
-H "X-API-Key: $ODDS_API_KEY" \
--data-urlencode "since=$RESUME" \
--data-urlencode 'catchup=true' \
--data-urlencode 'price_fields=all' \
--data-urlencode 'include_unavailable=true' \
--data-urlencode 'include_source=true'
Illustrative payload for an SSE delta event. The odd.id identifies the row to replace.
{
"changes": [
{
"odd": {
"bet_type": "moneyline",
"bookmaker": "sportsbet",
"event_id": "nbl-example-001",
"fair_odds": 1.95,
"id": "line-example-001",
"is_available": true,
"market_key": "moneyline",
"odds": 1.95,
"odds_no_vig": 1.96,
"period": "full game",
"selection_key": "selection-example-home",
"selection_name": "Tasmania JackJumpers",
"side": "home"
},
"op": "upsert"
}
],
"event_id": "nbl-example-001",
"resume": "1788472515000-0"
}
Discover matches with /v1/events, then snapshot and stream each match. Repeat discovery to add newly scheduled matches. This covers all markets offered through each event's odds endpoint.
Use /v1/odds/main-lines/snapshot?leagues=NBL and /v1/odds/main-lines/stream?leagues=NBL for a lighter NBL scoreboard. Main lines are a subset of the per-match markets.
delta: apply each change to the local odds row keyed by odd.id. Save the latest resume token.heartbeat: keep the connection open. It has no odds changes.resync: discard the local odds view, reload every snapshot page, then reopen the stream with the new token.since=<last_resume> and catchup=true. Back off after errors.The WebSocket route is /v1/events/{event_id}/odds/ws. It accepts the same query filters and sends JSON envelopes shaped as {"event":"delta","data":{...}}. Keep API keys on your server. Match discovery and results still use their REST endpoints.
For a line chart or audit, request /v1/events/{event_id}/odds/history with a selection_key from the snapshot and bounded ISO 8601 from_ts and to_ts values. The history stream follows one selection. Both history routes require history access; use the match snapshot and odds stream for current state.
Field dictionary
These fields are the pieces an NBL product needs to join matches, compare the same bet, show current prices, and recover a live connection. Optional fields can be absent or null.
| Field | What it means |
|---|---|
event_id | The canonical match ID. Use it in match, result, snapshot, and stream paths. |
sport / league | Use basketball and NBL to identify the competition in events and coverage. |
home_team / away_team | The clubs or teams assigned to the home and away sides. |
start_time | Scheduled tip-off time as Unix seconds. Convert it to the viewer's time zone. |
bookmakers | Bookmakers attached to the match. Check the odds snapshot for the actual selections. |
| Field | What it means |
|---|---|
id | An odds-row ID. Use it to update a row when a stream delta arrives; treat its format as opaque. |
bookmaker | The canonical bookmaker key. Keep it when comparing prices across books. |
market_key | The exact normalized market key in an odds row. The market_keys query filter matches this value. |
bet_type | The normalized market family, such as moneyline, handicap, total, or player prop. |
period / period_str | The period the bet covers. Keep it when matching selections. |
metric | The measured scoring or player-stat quantity when the market supplies one. |
line | The line or threshold. It is nullable and may be a string. |
side | The side of the market, such as home, away, over, or under. |
player_name | The player for a prop market, when present. |
selection_name | The display name of the offered selection, when present. |
selection_key | An optional normalized selection identity. Treat it as opaque and also compare period, line, player, and side. |
| Field | What it means |
|---|---|
odds | The bookmaker's decimal price. A value of 1.91 implies about a 52.4% break-even probability before fees. |
odds_no_vig | A nullable price with the bookmaker margin removed. |
fair_odds | A nullable composite fair-odds estimate. Request price_fields=all for every supported price field. |
is_available | Whether the row is currently offered. Request include_unavailable=true to track suspensions. |
as_of_ts_ms | Snapshot freshness time in Unix milliseconds. It is distinct from the match start time. |
ttl_seconds | The snapshot cache lifetime when supplied. Show age rather than presenting stale odds as current. |
next_cursor | The next page token. Continue until it is null for both event lists and odds snapshots. |
resume / since | Pass the snapshot resume token as the stream's since parameter, then persist the latest delta token. |
NBL player props are betting markets. The public contract does not expose a separate player box-score or play-by-play endpoint.
Production
| Response or state | What to do |
|---|---|
| No NBL events | Keep the selected time window visible. An empty page means no matches matched that request; widen the window or wait for another slate. |
| No odds rows | Keep the match record. Odds can be posted later, and a specific bookmaker or prop may have no rows. |
status=pending | The result record is not ready. Poll the match result endpoint separately from the odds stream. |
Old as_of_ts_ms | Show the snapshot age in your product and reload rather than presenting a stale price as current. |
| 401 or 403 | Check the server-side API key and whether the account has event odds access. See plans. |
| 429 or stream disconnect | Back off and reconnect with the last resume token. Respect rate-limit headers when present. |
resync | Reload all snapshot pages before opening a new stream. The older changes cannot safely be replayed. |
Keep ODDS_API_KEY on your server. Use the API reference for current response codes and parameter limits, and status and support when an endpoint is unavailable.
Python example
The Python example pages a rolling NBL schedule, requests match records and results, loads every odds snapshot page, and opens a stream for each match. It writes JSON Lines, so another process can consume the complete records. Set a suitable discovery window and connection budget for your product.
python -m pip install httpx
export ODDS_API_KEY='your_api_key'
python nbl_full_feed.py
Save nbl_full_feed.py and run it from your server. Set NBL_PAST_HOURS and NBL_FUTURE_HOURS to change the rolling match window. Set ODDS_API_BASE_URL for a different API environment.
"""Discover NBL games, load every available odds row, and follow each game.
Install: python -m pip install httpx
Run: ODDS_API_KEY=your_key python nbl_full_feed.py
The program writes JSON Lines to stdout. Set NBL_PAST_HOURS and
NBL_FUTURE_HOURS to change the rolling discovery window.
"""
from __future__ import annotations
import asyncio
import json
import os
import random
import sys
import time
from typing import Any, AsyncIterator
import httpx
BASE_URL = os.getenv("ODDS_API_BASE_URL", "https://api.odds-api.net/v1").rstrip("/") + "/"
API_KEY = os.getenv("ODDS_API_KEY", "").strip()
PAST_HOURS = int(os.getenv("NBL_PAST_HOURS", "8"))
FUTURE_HOURS = int(os.getenv("NBL_FUTURE_HOURS", "48"))
DISCOVERY_SECONDS = int(os.getenv("NBL_DISCOVERY_SECONDS", "300"))
ODDS_SHAPE = {
"price_fields": "all",
"include_unavailable": "true",
"include_source": "true",
}
def emit(kind: str, **payload: Any) -> None:
print(json.dumps({"kind": kind, **payload}, separators=(",", ":")), flush=True)
async def get_json(client: httpx.AsyncClient, path: str, params: dict | None = None) -> dict:
response = await client.get(path, params=params)
response.raise_for_status()
return response.json()
async def nbl_events(client: httpx.AsyncClient) -> list[dict]:
now = int(time.time())
params = {
"sport": "basketball",
"league": "NBL",
"start_from": now - PAST_HOURS * 3600,
"start_to": now + FUTURE_HOURS * 3600,
"limit": 1000,
}
events: list[dict] = []
while True:
page = await get_json(client, "events", params)
events.extend(page.get("items") or [])
cursor = page.get("next_cursor")
if not cursor:
return events
params["cursor"] = cursor
async def complete_snapshot(client: httpx.AsyncClient, event_id: str) -> tuple[dict, dict[str, dict]]:
params = {"limit": 10000, **ODDS_SHAPE}
rows: dict[str, dict] = {}
first_page: dict | None = None
while True:
page = await get_json(client, f"events/{event_id}/odds/snapshot", params)
if first_page is None:
first_page = page
for odd in page.get("items") or []:
if odd.get("id"):
rows[str(odd["id"])] = odd
cursor = page.get("next_cursor")
if not cursor:
break
params["cursor"] = cursor
# Start the stream at the first page's resume token. Changes that arrive
# while later snapshot pages load can then be replayed by the stream.
return first_page or {}, rows
async def sse_messages(response: httpx.Response) -> AsyncIterator[tuple[str, dict]]:
event = "message"
data: list[str] = []
async for line in response.aiter_lines():
if line == "":
if data:
yield event, json.loads("\n".join(data))
event, data = "message", []
elif line.startswith("event:"):
event = line[6:].strip()
elif line.startswith("data:"):
data.append(line[5:].lstrip())
if data:
yield event, json.loads("\n".join(data))
def apply_delta(rows: dict[str, dict], payload: dict) -> None:
for change in payload.get("changes") or []:
odd = change.get("odd") or {}
row_id = str(odd.get("id") or "")
if not row_id:
continue
if change.get("op") in {"remove", "delete"}:
rows.pop(row_id, None)
elif change.get("op") == "upsert":
# Keep is_available=false rows so the product can show suspensions.
rows[row_id] = odd
async def follow_event(client: httpx.AsyncClient, event_id: str) -> None:
resume = ""
rows: dict[str, dict] = {}
needs_snapshot = True
retry_seconds = 1.0
while True:
try:
if needs_snapshot:
snapshot, rows = await complete_snapshot(client, event_id)
resume = str(snapshot.get("resume") or "$")
emit(
"odds_snapshot",
event_id=event_id,
as_of_ts_ms=snapshot.get("as_of_ts_ms"),
ttl_seconds=snapshot.get("ttl_seconds"),
resume=resume,
items=list(rows.values()),
)
needs_snapshot = False
params = {"since": resume, "catchup": "true", "max_batch": 2000, **ODDS_SHAPE}
async with client.stream(
"GET", f"events/{event_id}/odds/stream", params=params,
headers={"Accept": "text/event-stream"},
) as response:
response.raise_for_status()
retry_seconds = 1.0
async for event, payload in sse_messages(response):
if event == "delta":
apply_delta(rows, payload)
resume = str(payload.get("resume") or resume)
emit("odds_delta", event_id=event_id, resume=resume, changes=payload.get("changes") or [])
elif event == "resync":
emit("odds_resync", event_id=event_id, reason=payload.get("reason"))
needs_snapshot = True
break
# A heartbeat contains no price change.
except asyncio.CancelledError:
raise
except (httpx.HTTPError, ValueError, json.JSONDecodeError) as exc:
emit("stream_error", event_id=event_id, error=str(exc))
if needs_snapshot:
continue
await asyncio.sleep(retry_seconds + random.uniform(0, 0.5))
retry_seconds = min(retry_seconds * 2, 30.0)
async def game_record(client: httpx.AsyncClient, event_id: str) -> None:
try:
detail, result = await asyncio.gather(
get_json(client, f"events/{event_id}"),
get_json(client, f"events/{event_id}/results"),
)
emit("event", event_id=event_id, data=detail)
emit("result", event_id=event_id, data=result)
except httpx.HTTPError as exc:
emit("record_error", event_id=event_id, error=str(exc))
async def bounded_game_record(client: httpx.AsyncClient, semaphore: asyncio.Semaphore, event_id: str) -> None:
async with semaphore:
await game_record(client, event_id)
async def main() -> None:
if not API_KEY:
raise SystemExit("Set ODDS_API_KEY before running this example.")
timeout = httpx.Timeout(connect=10.0, read=None, write=10.0, pool=10.0)
tasks: dict[str, asyncio.Task] = {}
record_limit = asyncio.Semaphore(6)
async with httpx.AsyncClient(
base_url=BASE_URL,
headers={"X-API-Key": API_KEY, "Accept": "application/json"},
timeout=timeout,
) as client:
try:
while True:
try:
events = await nbl_events(client)
ids = {str(item["event_id"]) for item in events if item.get("event_id")}
emit("event_list", count=len(events), items=events)
for event_id in ids:
if event_id not in tasks or tasks[event_id].done():
tasks[event_id] = asyncio.create_task(follow_event(client, event_id))
for event_id in set(tasks) - ids:
tasks.pop(event_id).cancel()
await asyncio.gather(
*(bounded_game_record(client, record_limit, event_id) for event_id in ids)
)
except httpx.HTTPError as exc:
emit("discovery_error", error=str(exc))
await asyncio.sleep(DISCOVERY_SECONDS)
finally:
for task in tasks.values():
task.cancel()
await asyncio.gather(*tasks.values(), return_exceptions=True)
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
print("Stopped.", file=sys.stderr)
Reference
The parameter lists come from the current public OpenAPI document. The full API reference contains response schemas and other sports routes.
/v1/coverageReturns public bookmaker, sport, league, and recently observed market coverage. Market records are approximate and based on normalized odds lines seen in the configured lookback window, not a guarantee that every market is available for every event at request time.
bookmakerCanonical bookmaker filter. Use `/bookmakers` or `/coverage` to discover supported keys.
sportSport filter. Use `/sports` to discover supported values.
leagueLeague filter. Use `/leagues?sport=...` to discover supported values.
country_codeComma-separated country code filter, for example `AU` or `AU,UK`.
lookback_daysNumber of days of recently observed approximate market coverage to include. Maximum is 90.
view`summary` returns the bookmaker, sport and league catalog without market rows (small and cacheable; use it for counts and pickers). `full` (default) also returns every observed market row.
/v1/bookmakersLists active bookmakers accepted by bookmaker filters across odds and betting endpoints. Each item includes the country codes where that bookmaker is available. Pass `country_code=AU` or `country_code=AU,UK` to filter the catalog.
country_codeComma-separated country code filter, for example `AU` or `AU,UK`.
/v1/eventsSearches sports events by sport, league, team, time window, status, bookmaker coverage, and pagination cursor. For production polling, use bounded time windows, keep filters stable across pages, and pass `next_cursor` back as `cursor` until there is no next cursor. For live discovery start with `/v1/events/live`; `event_states=in_play` returns only events with a fresh live observation and adds a `hint` when started but unconfirmed events were excluded. Every item has a plain `live_status`.
sportSport filter. Use `/sports` to discover supported values.
leagueLeague filter. Use `/leagues?sport=...` to discover supported values.
start_fromUnix seconds lower bound for event start time. Use bounded windows in production polling.
start_toUnix seconds upper bound for event start time. Keep windows narrow for hot sync jobs.
cursorPagination cursor from the previous `next_cursor`. Keep filters identical between pages.
limitMaximum items to return. Respect the caps returned by `/limits`.
include_bookmaker_idsWhen true, include bookmaker-to-odds-data IDs and preview ID maps on event responses.
include_sourceWhen true, include per-line/per-bookmaker provenance and capture metadata. Top-level freshness fields are always kept.
include_opportunity_countsnot_started_onlynot_started_buffer_secondsevent_statesLifecycle filter. Requesting in_play automatically applies the live lookback window.
live_candidatesInclude already-started events that remain candidates for live play; this is not confirmation of current play.
/v1/events/{event_id}Returns the current event record for a canonical sports event ID.
event_idrequiredCanonical event or race identifier from an event list response.
include_linksWhen true, include bookmaker/deep-link fields such as match links and racing links.
include_raw_payloadWhen true, include raw stored payload/data objects where the endpoint exposes them.
include_sourceWhen true, include per-line/per-bookmaker provenance and capture metadata. Top-level freshness fields are always kept.
include_bookmaker_idsWhen true, include bookmaker-to-odds-data IDs and preview ID maps on event responses.
include_debug_idsWhen true, include internal/subgroup/opposing IDs useful for reconciliation.
/v1/events/{event_id}/resultsReturns the latest known result for a sports event, or `pending` until settled. Poll every 1-5 minutes after start, then back off once the event is final.
event_idrequiredCanonical event or race identifier from an event list response.
/v1/events/{event_id}/odds/snapshotReturns the current odds lines for one event. Use filters to narrow bookmakers, market types, market keys, and periods. An explicit `bookmakers` filter returns every matching line for those bookmakers in one response, up to the 25,000-item safety cap. Without a bookmaker filter, pages contain whole bookmakers, so a bookmaker's matching markets are never split across pages. Follow the opaque `next_cursor` until `complete=true`. Cache by request shape. `as_of_ts_ms` is event-wide: the latest acceptance from any bookmaker on the event, including bookmakers this response does not return, so it can look fresh while the bookmakers you asked for are old. Use `bookmaker_as_of_ts_ms` for bookmaker-specific freshness and compare it with `target_refresh_interval_seconds`; `oldest_bookmaker_as_of_ts_ms`, `newest_bookmaker_as_of_ts_ms` and `stale_bookmakers` (returned bookmakers last accepted more than twice the target interval ago) summarise it (prediction markets report when their current contract prices were accepted). Respect `ttl_seconds` when present, and persist `resume` when you plan to subscribe to updates. Pass `price_fields=odds,fair` to include nullable composite fair odds alongside bookmaker odds. Exchange orderbooks are excluded from this sportsbook-style odds surface.
event_idrequiredCanonical event or race identifier from an event list response.
limitSoft item target when `bookmakers` is omitted. Pages contain whole bookmakers and may exceed this target. With an explicit bookmaker filter, all matching lines are returned up to the 25,000-item safety cap.
cursorOpaque bookmaker-page cursor from the previous `next_cursor`. Use only when `bookmakers` is omitted and keep all filters identical between pages.
bookmakersComma-separated bookmaker allow-list. Explicitly requested bookmakers are returned complete in one response, up to the 25,000-item safety cap. Use `/bookmakers` to discover supported keys. Requested keys this API key cannot access, or that are not bookmakers, are listed in `excluded_bookmakers` and the `X-Excluded-Bookmakers` header.
typesComma-separated market type allow-list for odds filters.
market_keysComma-separated market key allow-list for odds filters.
periodsComma-separated period allow-list for odds filters. Each value matches a line's numeric `period` or its period label, for example `full time`, `1st half` or `after 15 minutes`. Numeric `0` also matches timed checkpoints such as `after 15 minutes`; use `full time` to select only full-time lines.
price_fields`odds`, `odds,novig`, `odds,fair`, or `all`. Compact clients default to `odds`; existing clients default to current full pricing fields.
include_sourceWhen true, include per-line/per-bookmaker provenance and capture metadata. Top-level freshness fields are always kept.
include_debug_idsWhen true, include internal/subgroup/opposing IDs useful for reconciliation.
include_unavailableWhen true, include unavailable or suspended rows where the endpoint supports them.
include_staleReturn bookmakers whose prices are very stale (last accepted more than max(24 h, 20 x target_refresh_interval_seconds) ago, or only pre-match prices after kickoff). By default they are left out and listed in excluded_bookmakers with reason `stale`.
/v1/events/{event_id}/odds/streamServer-Sent Events feed for odds changes on one event. Subscribe after reading the snapshot and pass the snapshot `resume` value as `since` to receive catch-up changes when available. Handle ordered semantic `delta` batches idempotently and persist each batch's `resume`; a `heartbeat` carries current freshness even when prices did not change. Reload the snapshot after `resync`. Exchange orderbook changes are served only from the exchange orderbook stream.
event_idrequiredCanonical event or race identifier from an event list response.
bookmakersComma-separated bookmaker allow-list. Use `/bookmakers` to discover supported keys. Requested keys this API key cannot access, or that are not bookmakers, are reported in the `X-Excluded-Bookmakers` header.
typesComma-separated market type allow-list for odds filters.
market_keysComma-separated market key allow-list for odds filters.
periodsComma-separated period allow-list for odds filters. Each value matches a line's numeric `period` or its period label, for example `full time`, `1st half` or `after 15 minutes`. Numeric `0` also matches timed checkpoints such as `after 15 minutes`; use `full time` to select only full-time lines.
price_fields`odds`, `odds,novig`, `odds,fair`, or `all`. Compact clients default to `odds`; existing clients default to current full pricing fields.
include_sourceWhen true, include per-line/per-bookmaker provenance and capture metadata. Top-level freshness fields are always kept.
include_debug_idsWhen true, include internal/subgroup/opposing IDs useful for reconciliation.
include_unavailableWhen true, include unavailable or suspended rows where the endpoint supports them.
sinceResume token from a previous snapshot or stream message. Pass it after reconnecting.
catchupWhen true, return available missed stream events after `since` before waiting for new events.
heartbeat_secMaximum seconds between messages: a `heartbeat` is sent whenever this long passes without another message. Valid range is 5-120.
max_batchMaximum stream messages to read per batch. Default is 500; use smaller batches for low-latency clients.
/v1/events/{event_id}/odds/wsWebSocket feed for odds changes on one event. Messages use the same `delta`, `heartbeat`, and `resync` payloads as the SSE stream. Reconnect with jittered exponential backoff and `since=<last_resume>`.
event_idrequiredCanonical event or race identifier from an event list response.
bookmakersComma-separated bookmaker allow-list. Use `/bookmakers` to discover supported keys.
typesComma-separated market type allow-list for odds filters.
market_keysComma-separated market key allow-list for odds filters.
periodsComma-separated period allow-list for odds filters. Each value matches a line's numeric `period` or its period label, for example `full time`, `1st half` or `after 15 minutes`. Numeric `0` also matches timed checkpoints such as `after 15 minutes`; use `full time` to select only full-time lines.
price_fields`odds`, `odds,novig`, `odds,fair`, or `all`. Compact clients default to `odds`; existing clients default to current full pricing fields.
include_sourceWhen true, include per-line/per-bookmaker provenance and capture metadata. Top-level freshness fields are always kept.
include_debug_idsWhen true, include internal/subgroup/opposing IDs useful for reconciliation.
include_unavailableWhen true, include unavailable or suspended rows where the endpoint supports them.
sinceResume token from a previous snapshot or stream message. Pass it after reconnecting.
catchupWhen true, return available missed stream events after `since` before waiting for new events.
heartbeat_secMaximum seconds between messages: a `heartbeat` is sent whenever this long passes without another message. Valid range is 5-120.
max_batchMaximum stream messages to read per batch. Default is 500; use smaller batches for low-latency clients.
/v1/events/{event_id}/odds/historyReturns line movement for a single selection across bookmakers and a time range. Use `selection_key` from an odds snapshot response, bound queries with `from_ts` and `to_ts`, and narrow by bookmaker or market when building charts or backtests.
event_idrequiredCanonical event or race identifier from an event list response.
selection_keyrequiredStable selection identifier from an odds snapshot line, used for history and line movement.
market_group_idOptional market grouping filter for history queries.
bookmakersComma-separated bookmaker allow-list. Use `/bookmakers` to discover supported keys. Requested keys this API key cannot access, or that are not bookmakers, are reported in the `X-Excluded-Bookmakers` header.
from_tsISO8601 UTC start timestamp for a bounded history query.
to_tsISO8601 UTC end timestamp for a bounded history query.
price_typeHistory price type to return, for example odds.
price_fields`odds`, `odds,novig`, `odds,fair`, or `all`. Compact clients default to `odds`; existing clients default to current full pricing fields.
include_sourceWhen true, include per-line/per-bookmaker provenance and capture metadata. Top-level freshness fields are always kept.
include_debug_idsWhen true, include internal/subgroup/opposing IDs useful for reconciliation.
include_unavailableWhen true, include unavailable or suspended rows where the endpoint supports them.
limit_points_per_bookmakerMaximum history points per bookmaker. Use this to keep chart/backtest payloads bounded.
/v1/events/{event_id}/odds/history/streamServer-Sent Events feed for line movement on one selection. This is useful for charts that should update while an event market is moving.
event_idrequiredCanonical event or race identifier from an event list response.
selection_keyrequiredStable selection identifier from an odds snapshot line, used for history and line movement.
market_group_idOptional market grouping filter for history queries.
bookmakersComma-separated bookmaker allow-list. Use `/bookmakers` to discover supported keys. Requested keys this API key cannot access, or that are not bookmakers, are reported in the `X-Excluded-Bookmakers` header.
price_typeHistory price type to return, for example odds.
price_fields`odds`, `odds,novig`, `odds,fair`, or `all`. Compact clients default to `odds`; existing clients default to current full pricing fields.
include_sourceWhen true, include per-line/per-bookmaker provenance and capture metadata. Top-level freshness fields are always kept.
include_debug_idsWhen true, include internal/subgroup/opposing IDs useful for reconciliation.
include_unavailableWhen true, include unavailable or suspended rows where the endpoint supports them.
sinceResume token from a previous snapshot or stream message. Pass it after reconnecting.
catchupWhen true, return available missed stream events after `since` before waiting for new events.
heartbeat_secMaximum seconds between messages: a `heartbeat` is sent whenever this long passes without another message. Valid range is 5-120.
max_batchMaximum stream messages to read per batch. Default is 500; use smaller batches for low-latency clients.
For a narrower build, start with the NBL product overview or the player market key list.