Skip to content
Product Documentation

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

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.

Terminal window
curl -s "https://api.betflux.ai/v1/games/NBA_ORL_DET_20260503/closing-lines" \
-H "Authorization: Bearer $BETFLUX_API_KEY" -o closing.parquet

Responses carry an ETag (the artifact’s content hash) and Accept-Ranges: bytes; full responses are edge-cached for 24 hours.

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:

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

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, timestamp
FROM 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';

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 versions in GET /v1/datasets.
  • Every file names its version in a dataset_version column, and in the X-Betflux-Dataset-Version response 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;

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.

Terminal window
betflux get closing-lines --game NBA_ORL_DET_20260503 --format csv > lines.csv