Skip to content
Product Documentation

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.

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 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-lines segments 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_days of today is recorded as exempt and never counted.
  • At the allowance, only a new game is refused. That request gets 429 with problem type quota-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.

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.

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:

Terminal window
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.

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.

POST /v1/me/games/preview says what reading a list of games would cost right now, and counts nothing:

Terminal window
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.

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.

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.

  • 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 against games_remaining.
  • Prefer games already counted this period — betflux usage games lists them and betflux games --cost marks them — and narrow date ranges with the /v1/games filters (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); a quota-exhausted 429 raises QuotaExceededError immediately.
  • --limit / max_rows stops 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.