Rate limits & quotas
Two limits apply to an API key: a per-minute request limit, and on plans
that have one a games allowance per billing period. The rules on this page
are the same for every key. The numbers are not: your key’s own limits,
usage and reset come from GET /v1/me, and nothing here states them.
Rate limits
Section titled “Rate limits”Each plan allows a fixed number of requests per minute per API key; your
key’s limit is limits.rate_limit_per_minute on /v1/me. Exceeding it
returns 429 with problem type rate-limited and the RateLimit-* headers
described below. Back off and retry after Retry-After. Rate limits are
unchanged by games metering, and reading /v1/me costs one request like any
other.
A 429 with problem type throttled is an operations stop on a specific key
— see error guidance.
Usage = distinct games per billing period
Section titled “Usage = distinct games per billing period”Usage is metered in games, not requests or rows. A game is one public game
id (NFL_KC_BUF_20260920), and it counts once per billing period, on the
first successful read of any of its per-game datasets:
- the final Parquet artifacts —
closing-lines,market-results,sportsbook-lines,game-state-timeline; - live
sportsbook-linessegments and the board; - the sports data game files —
game-state,plays,player-stats,team-stats.
Everything after that first read is free until the period resets: the game’s
other datasets, re-reads, HTTP Range reads, cache hits and polls of a live
tail. A partial (Range) read counts the game like a full one — and from then
on the whole game is free. Listings, reference data (/v1/games, /v1/teams,
…), the league-scoped injuries dataset, 304s, 404s and 409s count
nothing.
Two rules depend on the plan:
- Recent games are free on plans with a recent-game window: a game dated
within
limits.free_recent_game_daysof today is recorded asexemptand never counted. - At the allowance, only a new game is refused. That request gets
429with problem typequota-exhausted. Every game already counted this period stays readable, in every dataset, until the period resets.
Filters never change what a game costs: the server streams whole files and the SDK/CLI filter locally after download. The lever you control is which games you read.
Billing periods
Section titled “Billing periods”With a subscription, the period is the monthly window stepped from your
billing anchor — so an annual plan still gets a monthly allowance; without
one it is the UTC calendar month. /v1/me reports usage.period_start and
usage.resets_at.
Checking your limits and usage
Section titled “Checking your limits and usage”GET /v1/me describes the calling key’s whole entitlement. Reading it never
counts a game, and it stays reachable when the allowance is spent:
curl -s "https://api.betflux.ai/v1/me" \ -H "Authorization: Bearer $BETFLUX_API_KEY"| Field | Meaning |
|---|---|
tier, plan.label, plan.size |
The tier code, the plan’s display name and, for plans sold in sizes, which size (null otherwise) |
limits.rate_limit_per_minute |
Requests allowed per minute on the channel the request came in on |
limits.games_per_period |
Games the plan allows per period; null means uncapped |
limits.free_recent_game_days |
Games dated within this many days are free; null when the plan has no such window |
limits.max_keys, limits.live, limits.league_datasets |
API keys the account may hold; whether live lines and league datasets are included |
limits.sample |
The sample rule on a trial key; null on every other plan |
usage.games_used, usage.games_allowance, usage.games_remaining |
The period’s counters; games_allowance and games_remaining are null when uncapped |
usage.games_exempt |
Games read free this period under the recent-game window |
usage.period_start, usage.resets_at |
When the current period began and when the counters reset (UTC instants) |
usage.counted_by_league |
Counted games per league, e.g. {"NFL": 120, "NBA": 40} — the ids themselves are listed by GET /v1/me/games |
usage.rows_used |
Rows served this period — telemetry, never a limit |
upgrade_url |
Where to change plan |
betflux keys check prints the same standing (--json prints the raw body
for scripts and agents to parse). The legacy quota block stays for one SDK
release with monthly_rows always null.
What a read cost
Section titled “What a read cost”On a capped plan every counted read carries two response headers:
| Header | Meaning |
|---|---|
x-betflux-game |
new — this read counted the game; already — counted earlier this period, free; exempt — within the recent-game window, free |
x-betflux-games-remaining |
New games the period still allows |
Uncapped plans send neither. The Python SDK surfaces them as GameDone.counted
and .games_remaining on progress events and as .cost on the result objects;
the CLI prints one stderr line after each read.
Pricing a pull before you make it
Section titled “Pricing a pull before you make it”POST /v1/me/games/preview says what reading a list of games would cost
right now, and counts nothing:
curl -s -X POST "https://api.betflux.ai/v1/me/games/preview" \ -H "Authorization: Bearer $BETFLUX_API_KEY" \ -H "Content-Type: application/json" \ -d '{"game_ids": ["NFL_KC_BUF_20260920", "NBA_DET_ORL_20260922"]}'The response lists each id in request order with its cost — new (a read
would count it), already (recorded this period, free), exempt (within the
recent-game window, free) or invalid (not a game id) — beside games_used,
games_allowance, games_remaining, fits (whether every new game fits in
what is left), over_by (how many do not) and enforced (whether the gateway
would refuse them today; while a plan’s allowance is still being measured
fits is always true). The body takes a bounded batch of ids, documented in
the OpenAPI spec; the SDK and betflux get … --dry-run chunk a larger pull for
you. If the usage ledger is unreachable the endpoint answers 503 rather than
guessing.
Which games are free to read again
Section titled “Which games are free to read again”GET /v1/me/games lists the games counted or exempt this period, newest read
first, each as {game_id, first_read_at, first_dataset, cost} with cost
counted or exempt. Filter with league, date_from and date_to
(inclusive, on the date in the game id); page with limit and the opaque
cursor from the previous page’s next_cursor. It counts nothing, and
answers 503 instead of an empty list when the ledger is unreachable.
betflux usage games and bf.counted_games() page through it;
betflux games --cost marks each listed game free_to_read.
Headers on 429
Section titled “Headers on 429”Successful responses carry no rate-limit headers — use /v1/me to see
your limits and usage. When a request is rejected with a rate-limit 429,
the response carries the
IETF RateLimit headers:
| Header | Meaning |
|---|---|
RateLimit-Limit |
Requests allowed per minute |
RateLimit-Remaining |
0 — the window is spent |
RateLimit-Reset |
Seconds until the window resets |
Retry-After |
Seconds to wait before retrying |
A quota-exhausted 429 also carries Retry-After, but it counts down to
the period reset — don’t sleep on it. Its body carries game_id,
games_used, games_allowance, reset_at and upgrade_url. Stop reading
new games until reset_at, or keep working with the games already counted.
Practical guidance
Section titled “Practical guidance”- One game answers many questions: after its first read, every dataset of a counted game is free to fetch and re-fetch all period. Query locally as often as you like.
- Preview a range pull before you make it (
betflux get … --dry-run,Dataset.preview(...)) and budget againstgames_remaining. - Prefer games already counted this period —
betflux usage gameslists them andbetflux games --costmarks them — and narrow date ranges with the/v1/gamesfilters (league, date range, team, status) so a pull reads only the games you need. - The Python SDK retries rate-limit 429s and 5xx automatically with backoff
that honors
Retry-After(capped at 120 s); aquota-exhausted429 raisesQuotaExceededErrorimmediately. --limit/max_rowsstops a range query after enough matching rows have been yielded. It limits output, not cost: a game’s file is downloaded — and the game counted — before its rows are filtered, and if filters match nothing every game in the range can still be read.