Skip to content
Product Documentation

Errors

Every error is an RFC 9457 problem document with Content-Type: application/problem+json — including unknown routes (not-found) and server crashes (internal-error), so you never have to parse text/plain. That holds on the dataset endpoints too: a successful response is a Parquet file, but any failure is problem+json.

{
"type": "https://api.betflux.ai/problems/rate-limited",
"title": "Rate limit exceeded",
"status": 429,
"detail": "Too many requests for the PRO tier. Slow down and retry after the RateLimit-Reset window."
}

Every body carries a stable type URI under https://api.betflux.ai/problems/. Dispatch on type, not on the English title — titles are prose and can change; the URIs are the contract (and three different 429s exist). Some errors carry extension members — a quota-exhausted 429 adds reset and upgrade_url; a subscription-required 402 adds upgrade_url.

type (suffix) Status Meaning
validation-error 400 A parameter failed validation — an unknown query parameter (they’re rejected, never ignored), an invalid league/status/limit, a malformed cursor or rows, or a /v1/games date range over the 366-day cap; detail says which
invalid-key 401 Missing, malformed, or unrecognized API key; detail says how to fix it
subscription-required 402 Key is valid but has no entitled tier — subscribe or renew at the body’s upgrade_url
key-disabled 403 Key revoked or suspended — mint a new one
history-restricted 403 The requested game is outside your plan’s history window
not-found 404 No such route, dataset, game, or per-game artifact
not-final 404 The game is still being served live — sportsbook-lines only; the body carries game_id and the two live URLs
live-segment-behind 409 A live segment holds fewer rows than the listing promised — list the segments again; carries game_id, seq and the requested rows
live-cursor-reset 409 The live log was rebuilt since your cursor was minted — list again without a cursor; carries game_id
rate-limited 429 Per-minute request limit hit — back off per Retry-After
quota-exhausted 429 Monthly row quota spent — carries reset and upgrade_url
throttled 429 Operations kill-switch on this key
internal-error 500 Unexpected server error — retry with backoff

Three distinct conditions share status 429 — the problem type tells them apart:

  • rate-limited — you outran your tier’s per-minute limit. Back off and retry after Retry-After (seconds); the response also carries the RateLimit-* headers (see Rate limits & quotas).
  • quota-exhausted — your monthly row quota is spent. Retrying won’t help: stop until the reset date in the body, or upgrade via upgrade_url.
  • throttled — operations temporarily throttled this key (kill-switch). Not load-related on your side; contact us via Discord.
  • key-disabled — the key was revoked or suspended. Mint a new one at account/api-keys.
  • history-restricted — the game you’re fetching a dataset file for is older than your tier’s history window. DEMO keys are pinned to a fixed sample month; tiers with a day-count window get an explicit 403 for games past it — requests are never silently clamped.

Both belong to the live sportsbook-lines routes, both mean list again, and neither costs quota or is ever cached:

  • live-segment-behind — GET …/segments/{seq}?rows=N found an object holding fewer than N rows. The URLs a /segments listing returns carry the row count the listing promised, and an object is only ever shorter than that when the live log was rebuilt underneath the listing. Fetch /segments again and use its URLs. The body carries game_id, seq and the rows you asked for.
  • live-cursor-reset — GET …/segments?cursor=S:R named a position the log no longer holds (S past the last segment, or R past that segment’s rows). A cursor a listing returned stays valid for as long as the log stands, so this means the log was rebuilt (a version bump, lost state). List again without a cursor. The body carries game_id.

GET /v1/games/{game_id}/{dataset} distinguishes three failures that share problem type not-found — the title says which:

  • Unknown dataset — the dataset name isn’t one of closing-lines, game-state-timeline, market-results, sportsbook-lines; detail lists the known names.
  • Game not found — the game id didn’t resolve at all (typo, or not a valid public id).
  • No closing lines for this game (or No market results …, No sportsbook lines …, No state timeline …) — the game exists but no artifact does: either the dataset doesn’t cover the game’s league, or the data isn’t materialized yet — artifacts appear shortly after a game settles. detail says which.

A fourth 404 has its own type. not-final means the game has not settled yet and its sportsbook-lines data is being served live: the body carries game_id and a live object with the game’s segments and board URLs. Follow those instead of retrying — the final artifact replaces them once the game settles. Only sportsbook-lines is served live; every other dataset answers not-found as above.

The live listing routes (…/sportsbook-lines/segments and …/board) answer not-found with a reason member: final_available when the game has settled — the body’s final is the dataset route to fetch instead — and no_live_artifact when the game is not being served live at all.

  • rate-limited and 5xx: retry with exponential backoff, honoring the Retry-After header. The Python SDK does this automatically (with Retry-After capped at 120 s).
  • quota-exhausted and throttled: don’t retry in a loop — quota clears at reset; a throttle clears when operations lift it.
  • Other 4xx: fix the request; retrying won’t help.