Response formats
The API has two payload shapes:
| Surface | Content type | Shape |
|---|---|---|
Datasets — GET /v1/games/{game_id}/{dataset} |
application/vnd.apache.parquet |
One typed, compressed Parquet file per game |
Reference data — /v1/games, /v1/leagues, /v1/teams, /v1/players — and discovery — /v1/datasets, /v1/me |
application/json |
JSON objects / {data, next_cursor} envelopes |
Why Parquet
Section titled “Why Parquet”Dataset row counts vary; a sportsbook-lines game can contain roughly 190k
rows. See each dataset’s schema for its current columns.
At that size Parquet beats JSON on every axis that matters: columns carry real
types (int64 timestamps, doubles, string lists — no string re-parsing),
compression is columnar (a sportsbook-lines game is tens of MB as Parquet vs.
hundreds as JSON), and every analytical tool — DuckDB, pyarrow, pandas,
polars — reads it natively. The server streams the file byte-for-byte and
never filters, sorts, or re-encodes rows; queries run on your CPU.
curl -s "https://api.betflux.ai/v1/games/NBA_ORL_DET_20260503/closing-lines" \ -H "Authorization: Bearer $BETFLUX_API_KEY" -o closing.parquetResponses carry an ETag (the artifact’s content hash) and
Accept-Ranges: bytes; full responses are edge-cached for 24 hours.
HTTP Range / partial reads
Section titled “HTTP Range / partial reads”Standard byte ranges are honored (206 Partial Content with
Content-Range), which is what lets Parquet-aware readers fetch only the
footer and the column chunks a query touches:
curl -s "https://api.betflux.ai/v1/games/NBA_ORL_DET_20260503/closing-lines" \ -H "Authorization: Bearer $BETFLUX_API_KEY" -H "Range: bytes=0-65535"Note that a partial read counts the game exactly like a full download — the unit is distinct games per billing period, so the first read of a game counts it once and every later read, Range or not, is free (see Rate limits & quotas).
DuckDB straight at the files
Section titled “DuckDB straight at the files”The dataset endpoints are plain authenticated Parquet URLs, so DuckDB can query them in place — predicate pushdown means it reads only the byte ranges it needs:
CREATE SECRET betflux ( TYPE http, EXTRA_HTTP_HEADERS MAP {'Authorization': 'Bearer bfx_live_...'});SELECT operator, market_type, odds, timestampFROM read_parquet('https://api.betflux.ai/v1/games/MLB_BOS_NYY_20260715/sportsbook-lines')WHERE market_type = 'MONEYLINE'ORDER BY timestamp;Each file’s ETag is a hash of its contents. DuckDB compares it across
the requests one query makes. If a game’s file is rebuilt while your query
is reading it, DuckDB stops with ETag on reading file … the remote file has changed. Nothing is mixed: run the query again to read the new file.
Downloaded files work the same way, minus the auth setup:
SELECT * FROM 'closing.parquet' WHERE operator = 'PINNACLE';Reading many games at once
Section titled “Reading many games at once”Each game’s file is built at a dataset version, and the games in one range can be at different versions:
- A newer version only adds columns, or carries a change documented under
versionsinGET /v1/datasets. - Every file names its version in a
dataset_versioncolumn, and in theX-Betflux-Dataset-Versionresponse header. - Files built before that column existed lack it.
So read several files with union_by_name. Without it, DuckDB either
refuses the mismatched schemas or silently drops the columns only some
files have:
SELECT dataset_version, count(*)FROM read_parquet(['closing-1.parquet', 'closing-2.parquet'], union_by_name = true)GROUP BY dataset_version;Table / CSV / JSON output is client-side
Section titled “Table / CSV / JSON output is client-side”The CLI’s --format flag (table, record, json, jsonl, csv) renders
locally after download — same UX as ever, no server involvement. The compact
table view is a display-time projection; json, jsonl, and csv always
carry every column. See the CLI guide.
betflux get closing-lines --game NBA_ORL_DET_20260503 --format csv > lines.csv