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.
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, 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 |
The three 429s
Section titled “The three 429s”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 afterRetry-After(seconds); the response also carries theRateLimit-*headers (see Rate limits & quotas).quota-exhausted— your monthly row quota is spent. Retrying won’t help: stop until theresetdate in the body, or upgrade viaupgrade_url.throttled— operations temporarily throttled this key (kill-switch). Not load-related on your side; contact us via Discord.
The two 403s
Section titled “The two 403s”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.
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 public 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 — quota clears atreset; a throttle clears when operations lift it.- Other 4xx: fix the request; retrying won’t help.