# 1. List what exists
curl "https://api.gammaflip.io/api/v1/history/files/tradfi/QQQ?start=2026-10-01" \
-H "X-API-Key: gex_sk_your_key_here"
# 2. Ask for the file: the response is a 302, the link is in redirect_url
URL=$(curl -s -o /dev/null -w '%{redirect_url}' \
-H "X-API-Key: gex_sk_your_key_here" \
"https://api.gammaflip.io/api/v1/history/files/tradfi/QQQ/2026-10-05/strikes_total")
# 3. Download from the link, without your API key
curl -sS -o 2026-10-05_strikes_total.csv.gz "$URL"
import pandas as pd
import requests
BASE = "https://api.gammaflip.io/api/v1/history/files"
H = {"X-API-Key": "gex_sk_your_key_here"}
# Do not follow the redirect automatically: that would send the API key to storage.
r = requests.get(f"{BASE}/tradfi/QQQ/2026-10-05/strikes_total",
headers=H, allow_redirects=False)
if r.status_code != 302:
raise RuntimeError(r.json())
url = r.headers["Location"]
# The link has a query string, so pandas cannot infer the compression: say it.
df = pd.read_csv(url, compression="gzip")
print(df[["timestamp_utc", "strike", "strike_call_gex", "strike_put_gex"]].head())
import pandas as pd
import requests
BASE = "https://api.gammaflip.io/api/v1/history/files/tradfi/QQQ"
H = {"X-API-Key": "gex_sk_your_key_here"}
manifest = requests.get(BASE, headers=H, params={"start": "2026-09-01"}).json()["data"]
days = sorted({m["session_date"] for m in manifest if m["file"] == "levels"})
frames = []
for day in days:
r = requests.get(f"{BASE}/{day}/levels", headers=H, allow_redirects=False)
if r.status_code == 429:
break # out of file credits: resume after X-History-Files-Reset
frames.append(pd.read_csv(r.headers["Location"], compression="gzip"))
levels = pd.concat(frames, ignore_index=True)
total = levels[levels["expiration"] == "TOTAL"]
{
"data": [
{ "session_date": "2026-10-05", "file": "levels", "rows": 1240,
"bytes": 61234, "sha256": "9f2c...e1", "schema_version": 1 },
{ "session_date": "2026-10-05", "file": "strikes_total", "rows": 2700,
"bytes": 148112, "sha256": "41ab...07", "schema_version": 1 },
{ "session_date": "2026-10-05", "file": "strikes_by_expiry", "rows": 64000,
"bytes": 2210044, "sha256": "c07d...3a", "schema_version": 1 }
],
"meta": {
"timestamp": "2026-10-06T10:00:00.000000+00:00",
"exchange": "tradfi",
"coin": "QQQ",
"symbol": "QQQ",
"exchange_resolved": "tradfi",
"market": "tradfi",
"units": "USD per 1% move of the underlying",
"docs_url": "https://docs.gammaflip.io/data-dictionary",
"rate_limit": { "limit": 500, "remaining": 494, "reset": "2026-10-07T00:00:00+00:00" }
}
}
HTTP/1.1 302 Found
Location: https://...presigned-link...
Cache-Control: no-store
X-History-Files-Limit: 200
X-History-Files-Remaining: 199
X-History-Files-Reset: 2026-10-07T00:00:00+00:00
{
"error": "history_files_limit_exceeded",
"message": "Daily limit of 200 history files reached.",
"rate_limit": { "limit": 200, "remaining": 0, "reset": "2026-10-07T00:00:00+00:00" }
}
{
"error": "no_file",
"message": "No strikes_total file for QQQ on 2026-10-04."
}
History
History: Files
List and download daily CSV files with the full stored history
GET
/
history
/
files
/
{exchange}
/
{coin}
# 1. List what exists
curl "https://api.gammaflip.io/api/v1/history/files/tradfi/QQQ?start=2026-10-01" \
-H "X-API-Key: gex_sk_your_key_here"
# 2. Ask for the file: the response is a 302, the link is in redirect_url
URL=$(curl -s -o /dev/null -w '%{redirect_url}' \
-H "X-API-Key: gex_sk_your_key_here" \
"https://api.gammaflip.io/api/v1/history/files/tradfi/QQQ/2026-10-05/strikes_total")
# 3. Download from the link, without your API key
curl -sS -o 2026-10-05_strikes_total.csv.gz "$URL"
import pandas as pd
import requests
BASE = "https://api.gammaflip.io/api/v1/history/files"
H = {"X-API-Key": "gex_sk_your_key_here"}
# Do not follow the redirect automatically: that would send the API key to storage.
r = requests.get(f"{BASE}/tradfi/QQQ/2026-10-05/strikes_total",
headers=H, allow_redirects=False)
if r.status_code != 302:
raise RuntimeError(r.json())
url = r.headers["Location"]
# The link has a query string, so pandas cannot infer the compression: say it.
df = pd.read_csv(url, compression="gzip")
print(df[["timestamp_utc", "strike", "strike_call_gex", "strike_put_gex"]].head())
import pandas as pd
import requests
BASE = "https://api.gammaflip.io/api/v1/history/files/tradfi/QQQ"
H = {"X-API-Key": "gex_sk_your_key_here"}
manifest = requests.get(BASE, headers=H, params={"start": "2026-09-01"}).json()["data"]
days = sorted({m["session_date"] for m in manifest if m["file"] == "levels"})
frames = []
for day in days:
r = requests.get(f"{BASE}/{day}/levels", headers=H, allow_redirects=False)
if r.status_code == 429:
break # out of file credits: resume after X-History-Files-Reset
frames.append(pd.read_csv(r.headers["Location"], compression="gzip"))
levels = pd.concat(frames, ignore_index=True)
total = levels[levels["expiration"] == "TOTAL"]
{
"data": [
{ "session_date": "2026-10-05", "file": "levels", "rows": 1240,
"bytes": 61234, "sha256": "9f2c...e1", "schema_version": 1 },
{ "session_date": "2026-10-05", "file": "strikes_total", "rows": 2700,
"bytes": 148112, "sha256": "41ab...07", "schema_version": 1 },
{ "session_date": "2026-10-05", "file": "strikes_by_expiry", "rows": 64000,
"bytes": 2210044, "sha256": "c07d...3a", "schema_version": 1 }
],
"meta": {
"timestamp": "2026-10-06T10:00:00.000000+00:00",
"exchange": "tradfi",
"coin": "QQQ",
"symbol": "QQQ",
"exchange_resolved": "tradfi",
"market": "tradfi",
"units": "USD per 1% move of the underlying",
"docs_url": "https://docs.gammaflip.io/data-dictionary",
"rate_limit": { "limit": 500, "remaining": 494, "reset": "2026-10-07T00:00:00+00:00" }
}
}
HTTP/1.1 302 Found
Location: https://...presigned-link...
Cache-Control: no-store
X-History-Files-Limit: 200
X-History-Files-Remaining: 199
X-History-Files-Reset: 2026-10-07T00:00:00+00:00
{
"error": "history_files_limit_exceeded",
"message": "Daily limit of 200 history files reached.",
"rate_limit": { "limit": 200, "remaining": 0, "reset": "2026-10-07T00:00:00+00:00" }
}
{
"error": "no_file",
"message": "No strikes_total file for QQQ on 2026-10-04."
}
What it is
For every symbol and completed session there are three gzip-compressed CSV files, built once the session is complete. They hold the full stored history, including every expiration and every strike, and are meant for backtests, spreadsheets and data pipelines. Two endpoints:| Endpoint | Returns | Cost |
|---|---|---|
GET /history/files/{exchange}/{coin} | JSON list of the available files (manifest) | 1 request |
GET /history/files/{exchange}/{coin}/{date}/{file} | 302 redirect to the file | 1 file credit |
file | One row per | Use it for |
|---|---|---|
levels | capture x expiration (including TOTAL) | Levels of every expiration over a day |
strikes_total | capture x strike, TOTAL only, with the snapshot fields repeated on each row | The easiest file for strike-level backtests |
strikes_by_expiry | capture x expiration x strike | Per-expiration strike detail. Join to levels on timestamp_utc + expiration |
List files
GET /history/files/{exchange}/{coin} costs 1 request.
string
required
bybit, deribit, okx, derive, combined or tradfi. Venue subsets return
400 history_not_available.string
required
Symbol, for example
BTC, QQQ. Case-insensitive.string
First session date,
YYYY-MM-DD, inclusive. Default: the earliest.
Years 2020 to 2100 are accepted.string
Last session date,
YYYY-MM-DD, exclusive. Default: the latest. start on
or after end returns 400 invalid_param.array
Newest session first, at most 400 rows. Use
start / end to page through
longer ranges. Each row: session_date, file (levels, strikes_total or
strikes_by_expiry), rows, bytes, sha256 (of the gzip file) and
schema_version.Download a file
GET /history/files/{exchange}/{coin}/{date}/{file} answers 302 Found with a
Location header: a presigned link to the file that is valid for up to 15
minutes. Request the link again whenever you need it. The 302 is sent with
Cache-Control: no-store, so do not cache it either.
Use GET. A HEAD request returns 405 method_not_allowed (with Allow: GET)
and costs no credit.
string
required
Session date,
YYYY-MM-DD.string
required
levels, strikes_total or strikes_by_expiry.application/gzip and downloads as
<date>_<file>.csv.gz, for example 2026-10-05_strikes_total.csv.gz.
The presigned link is self-authenticating. Fetch it without your
X-API-Key
header. Most HTTP clients, curl -L included, forward custom headers to the
redirect target, so follow the redirect in two steps as shown below.Credits
A successful download costs one file credit from the separatehistory_files
daily bucket, not a request. A 400, 404 or 503 never costs a credit, because
the input, the file and the link are checked and prepared first. The manifest listing is not a download:
it costs a normal request. Limits and headers are in
Rate limits; see your balance in
/me.
The 302 and the 429 carry:
| Header | Meaning |
|---|---|
X-History-Files-Limit | Your daily file credits (0 = unlimited) |
X-History-Files-Remaining | Credits left until reset (0 together with a 0 limit means unlimited) |
X-History-Files-Reset | When the bucket resets (ISO 8601, UTC) |
Retry-After header is sent; read
X-History-Files-Reset. Browser clients can read these headers (they are listed
in CORS Access-Control-Expose-Headers).
Errors
error
No such file for that exchange, symbol, date and kind: the date is outside
coverage or not built yet. Check the manifest or
/history/coverage. Costs no credit.error
The daily file credits are used up. The body repeats the figures under
rate_limit, and the X-History-Files-* headers are set.error
File storage is temporarily unavailable. Retry later. Costs no credit.
error
file is not one of the three kinds (the body includes valid_values), or a
date is not YYYY-MM-DD. On the listing: a year outside 2020 to 2100, or
start on or after end.error
The download was requested with
HEAD. Use GET. Costs no credit.error
Venue subset. The body includes
valid_exchanges.# 1. List what exists
curl "https://api.gammaflip.io/api/v1/history/files/tradfi/QQQ?start=2026-10-01" \
-H "X-API-Key: gex_sk_your_key_here"
# 2. Ask for the file: the response is a 302, the link is in redirect_url
URL=$(curl -s -o /dev/null -w '%{redirect_url}' \
-H "X-API-Key: gex_sk_your_key_here" \
"https://api.gammaflip.io/api/v1/history/files/tradfi/QQQ/2026-10-05/strikes_total")
# 3. Download from the link, without your API key
curl -sS -o 2026-10-05_strikes_total.csv.gz "$URL"
import pandas as pd
import requests
BASE = "https://api.gammaflip.io/api/v1/history/files"
H = {"X-API-Key": "gex_sk_your_key_here"}
# Do not follow the redirect automatically: that would send the API key to storage.
r = requests.get(f"{BASE}/tradfi/QQQ/2026-10-05/strikes_total",
headers=H, allow_redirects=False)
if r.status_code != 302:
raise RuntimeError(r.json())
url = r.headers["Location"]
# The link has a query string, so pandas cannot infer the compression: say it.
df = pd.read_csv(url, compression="gzip")
print(df[["timestamp_utc", "strike", "strike_call_gex", "strike_put_gex"]].head())
import pandas as pd
import requests
BASE = "https://api.gammaflip.io/api/v1/history/files/tradfi/QQQ"
H = {"X-API-Key": "gex_sk_your_key_here"}
manifest = requests.get(BASE, headers=H, params={"start": "2026-09-01"}).json()["data"]
days = sorted({m["session_date"] for m in manifest if m["file"] == "levels"})
frames = []
for day in days:
r = requests.get(f"{BASE}/{day}/levels", headers=H, allow_redirects=False)
if r.status_code == 429:
break # out of file credits: resume after X-History-Files-Reset
frames.append(pd.read_csv(r.headers["Location"], compression="gzip"))
levels = pd.concat(frames, ignore_index=True)
total = levels[levels["expiration"] == "TOTAL"]
{
"data": [
{ "session_date": "2026-10-05", "file": "levels", "rows": 1240,
"bytes": 61234, "sha256": "9f2c...e1", "schema_version": 1 },
{ "session_date": "2026-10-05", "file": "strikes_total", "rows": 2700,
"bytes": 148112, "sha256": "41ab...07", "schema_version": 1 },
{ "session_date": "2026-10-05", "file": "strikes_by_expiry", "rows": 64000,
"bytes": 2210044, "sha256": "c07d...3a", "schema_version": 1 }
],
"meta": {
"timestamp": "2026-10-06T10:00:00.000000+00:00",
"exchange": "tradfi",
"coin": "QQQ",
"symbol": "QQQ",
"exchange_resolved": "tradfi",
"market": "tradfi",
"units": "USD per 1% move of the underlying",
"docs_url": "https://docs.gammaflip.io/data-dictionary",
"rate_limit": { "limit": 500, "remaining": 494, "reset": "2026-10-07T00:00:00+00:00" }
}
}
HTTP/1.1 302 Found
Location: https://...presigned-link...
Cache-Control: no-store
X-History-Files-Limit: 200
X-History-Files-Remaining: 199
X-History-Files-Reset: 2026-10-07T00:00:00+00:00
{
"error": "history_files_limit_exceeded",
"message": "Daily limit of 200 history files reached.",
"rate_limit": { "limit": 200, "remaining": 0, "reset": "2026-10-07T00:00:00+00:00" }
}
{
"error": "no_file",
"message": "No strikes_total file for QQQ on 2026-10-04."
}
The values above are illustrative. Verify a download against the manifest’s
sha256 and bytes if you need to.