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.
Problem types
Section titled “Problem types”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 |
The three 429s
Section titled “The three 429s”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 afterRetry-After(seconds); the response also carries theRateLimit-*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 carriesgame_id,games_used,games_allowance,reset_at(andreset, its date) andupgrade_url. Retrying the same game won’t help beforereset_at— but every game already counted this period stays readable, in every dataset, so keep working with those (GET /v1/me/gameslists them). See Rate limits & quotas.throttled— operations temporarily throttled this key (kill-switch). Not load-related on your side; contact us via Discord.
The three 403s
Section titled “The three 403s”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 livesportsbook-linesroutes (…/sportsbook-lines/segmentsand…/board;limits.liveon/v1/me) and the league-scoped datasets such asinjuries(/v1/leagues/{league}/…;limits.league_datasets). The body carriesfeature(liveorleague_datasets),required_tiers(the plans that include it, cheapest first) andupgrade_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 finalsportsbook-linesfile 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 carriesupgrade_urlandsample: "first-weekend". Read a sample-day game or upgrade; retrying the same game will not help.
The two 409s
Section titled “The two 409s”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=Nfound an object holding fewer thanNrows. The URLs a/segmentslisting 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/segmentsagain and use its URLs. The body carriesgame_id,seqand therowsyou asked for.live-cursor-reset—GET …/segments?cursor=S:Rnamed a position the log no longer holds (Spast the last segment, orRpast 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 carriesgame_id.
The 404s on dataset endpoints
Section titled “The 404s on dataset endpoints”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 ofclosing-lines,game-state-timeline,market-results,sportsbook-lines;detaillists 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(orNo 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.detailsays 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.
Retry guidance
Section titled “Retry guidance”rate-limitedand 5xx: retry with exponential backoff, honoring theRetry-Afterheader. The Python SDK does this automatically (withRetry-Aftercapped at 120 s).quota-exhaustedandthrottled: don’t retry in a loop — the allowance resets atreset_at, and games already counted can be read in the meantime; a throttle clears when operations lift it.plan-excludesandhistory-restricted: never retry on the same key — the plan decides, not timing. Skip the feature or the game, or followupgrade_url.- Other 4xx: fix the request; retrying won’t help.