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. 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 — for example, a quota-exhausted 429 names the game it refused and when the period resets.

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, a /v1/games date range over the 366-day cap, or a malformed /v1/me/games/preview body; detail says which
invalid-key 401 Missing, malformed, or unrecognized API key; detail says how to fix it
subscription-required 402 The key is valid, but the account has no active subscription. The response includes upgrade_url.
key-disabled 403 Key revoked or suspended — mint a new one
plan-excludes 403 Your plan does not include this feature (live sportsbook-lines or a league dataset such as injuries) — carries feature, required_tiers and upgrade_url; never worth retrying on this key
history-restricted 403 A trial key asked for a game outside the first-weekend sample — carries upgrade_url and sample: "first-weekend"; pick a sample-day game or upgrade
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 The period’s games allowance is spent and this game would be a new one — carries game_id, games_used, games_allowance, reset_at and upgrade_url; games already counted stay readable
throttled 429 Operations kill-switch on this key
internal-error 500 Unexpected server error — retry with backoff
internal-error 503 Usage ledger unavailable: /v1/me/games/preview or /v1/me/games could not reach the usage ledger and would rather not guess — nothing was counted; retry shortly

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

  • rate-limited — you outran the per-minute request limit. Back off and retry after Retry-After (seconds); the response also carries the RateLimit-* headers (see Rate limits & quotas).
  • quota-exhausted — the period’s games allowance is spent and the game you asked for would be a new one. The body carries game_id, games_used, games_allowance, reset_at (and reset, its date) and upgrade_url. Retrying the same game won’t help before reset_at — but every game already counted this period stays readable, in every dataset, so keep working with those (GET /v1/me/games lists them). See Rate limits & quotas.
  • 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.
  • plan-excludes — your plan does not include the feature the route serves. Two features are per plan: the live sportsbook-lines routes (…/sportsbook-lines/segments and …/board; limits.live on /v1/me) and the league-scoped datasets such as injuries (/v1/leagues/{league}/…; limits.league_datasets). The body carries feature (live or league_datasets), required_tiers (the plans that include it, cheapest first) and upgrade_url. The answer comes before any cache, file or usage is touched, so nothing was counted, and it is never worth retrying on the same key — switch plan or skip the feature. The settled game’s final sportsbook-lines file on the dataset route is open to every plan.
  • history-restricted — a trial key (the free sample) asked for a game outside the sample. The sample is every game dated the first Saturday of a month or the Sunday after it, by the date in the game id, in every per-game dataset and with no games cap; any other date is refused, never silently trimmed, so an edge-cached file can never leak past the rule. The body carries upgrade_url and sample: "first-weekend". Read a sample-day game or upgrade; retrying the same game will not help.

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 game 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 — the allowance resets at reset_at, and games already counted can be read in the meantime; a throttle clears when operations lift it.
  • plan-excludes and history-restricted: never retry on the same key — the plan decides, not timing. Skip the feature or the game, or follow upgrade_url.
  • Other 4xx: fix the request; retrying won’t help.