The live options chain is solved. Polygon, Tradier, ThetaData, and a dozen others ship raw option quotes for "now." The historical chain at minute resolution - every strike, every expiry, greeks attached - is much harder to find. You can patch it together from EOD snapshots if you don't mind losing the intraday signal, or you can pull raw tick data and run your own BSM pipeline if you have a quarter to spare.
The other path: one HTTP call against FlashAlpha's Historical API. This article walks through the chain endpoint specifically, with the patterns you'll actually use.
The Endpoint in One Line
GET https://historical.flashalpha.com/v1/optionquote/{ticker}?at=&expiry=&strike=&type=
Same shape as live api.flashalpha.com/v1/optionquote. Same fields. The only difference is the required at parameter - the as-of timestamp.
| Param | In | Required | Description |
ticker | path | yes | Underlying symbol (SPY today; more on demand) |
at | query | yes | yyyy-MM-ddTHH:mm:ss ET wall-clock, or yyyy-MM-dd (defaults to 16:00 ET) |
expiry | query | no | yyyy-MM-dd filter |
strike | query | no | Numeric strike filter |
type | query | no | C / Call / P / Put |
With all three of expiry, strike, type → returns a single object. Otherwise → array.
What Comes Back
{
"type": "C",
"expiry": "2026-03-06",
"strike": 680,
"bid": 3.42,
"ask": 3.44,
"mid": 3.43,
"lastUpdate": "2026-03-05T15:30:00",
"implied_vol": 0.2579,
"delta": 0.4824,
"gamma": 0.0435,
"theta": -1.8617,
"vega": 0.1417,
"rho": 0.0089,
"vanna": 0.0891,
"charm": -0.0147,
"open_interest": 6284
}
Every field that matters for analytics: prices, full BSM greeks (including vanna and charm), open interest. The greeks are recomputed from minute-level spot and IV - they're real intraday values, not EOD snapshots smeared across the day.
Pattern 1: Single Contract at a Moment
Pass all three filters - get one row.
import httpx
API_KEY = "..."
r = httpx.get(
"https://historical.flashalpha.com/v1/optionquote/SPY",
params={
"at": "2024-08-05T10:00:00",
"expiry": "2024-08-05",
"strike": 530,
"type": "P",
},
headers={"X-Api-Key": API_KEY},
).json()
print(f"SPY 530P 0DTE at 10:00 on Aug 5: mid={r['mid']:.2f}, IV={r['implied_vol']:.2%}")
Use this when you have a specific contract you want to track - backtesting a known straddle, replaying a single trade, or sanity-checking that a historical price is reasonable.
Pattern 2: Full Expiry at a Moment
Drop strike and type - get every contract for one expiry.
chain = httpx.get(
"https://historical.flashalpha.com/v1/optionquote/SPY",
params={"at": "2024-08-05T10:00:00", "expiry": "2024-08-05"},
headers={"X-Api-Key": API_KEY},
).json()
import pandas as pd
df = pd.DataFrame(chain)
print(df[["type", "strike", "mid", "implied_vol", "delta", "open_interest"]].head())
This is your standard "give me the chain" call. About 200-400 contracts per SPY expiry depending on tail extension. Useful for any single-expiry analysis: skew profile at a moment, OI distribution, dealer-positioning per strike.
Pattern 3: Full Chain at a Moment (Every Expiry)
Drop all filters - get every contract on every expiry.
full_chain = httpx.get(
"https://historical.flashalpha.com/v1/optionquote/SPY",
params={"at": "2024-08-05T10:00:00"},
headers={"X-Api-Key": API_KEY},
timeout=60,
).json()
print(f"{len(full_chain)} contracts in SPY chain at 10:00 on Aug 5, 2024")
Typically 6,000-10,000 contracts for SPY. This is the "rebuild the universe" call. Use it for full-surface fitting, total-OI calculations, or anything that needs the entire chain in one snapshot. One call, one timestamp, the whole chain.
Pattern 4: Time Series of a Single Contract
Loop over at values to build the price/greek history of one contract:
from datetime import datetime, timedelta
start = datetime(2024, 8, 5, 9, 30)
end = datetime(2024, 8, 5, 16, 0)
rows = []
t = start
with httpx.Client(headers={"X-Api-Key": API_KEY}, timeout=30) as c:
while t <= end:
r = c.get(
"https://historical.flashalpha.com/v1/optionquote/SPY",
params={
"at": t.strftime("%Y-%m-%dT%H:%M:%S"),
"expiry": "2024-08-05",
"strike": 530,
"type": "P",
},
)
if r.status_code == 200:
j = r.json()
rows.append({"t": t, "mid": j["mid"], "iv": j["implied_vol"], "delta": j["delta"]})
t += timedelta(minutes=15)
ts = pd.DataFrame(rows).set_index("t")
ts["mid"].plot(title="SPY 530P 0DTE - intraday on Aug 5, 2024 (carry-trade unwind)")
15-minute resolution gives you 27 samples across a session - usually enough for charts and intraday analysis without burning quota. Drop to 1-minute for trade-level reconstructions.
Pattern 5: Multi-Expiry Sampling at One Moment
For surface-shape analysis or term-structure plots, fetch each expiry separately rather than the full chain - usually faster and easier to align:
expiries = ["2024-08-05", "2024-08-09", "2024-08-16",
"2024-08-30", "2024-09-20", "2024-12-20"]
slices = {}
with httpx.Client(headers={"X-Api-Key": API_KEY}, timeout=30) as c:
for exp in expiries:
slices[exp] = c.get(
"https://historical.flashalpha.com/v1/optionquote/SPY",
params={"at": "2024-08-05T10:00:00", "expiry": exp},
).json()
Six calls, six DataFrames, ready for term-structure plotting. For higher-level surface work, the dedicated IV-surface endpoint ships a 50×50 grid in one call - use it when you don't need contract-level detail.
What's Honestly Missing
Three fields that exist in the response but are not populated historically - call this out in your code rather than letting it surprise you:
volume always 0. The minute-resolution options table doesn't carry contract volume. Use open_interest for liquidity ranking.
bidSize / askSize always 0. Same reason. Bid/ask prices themselves are populated normally.
svi_vol always null with "svi_vol_gated": "backtest_mode". SVI parameters are stored at the expiry level, but per-contract svi_vol derivation isn't plumbed in the historical service yet. Use implied_vol (BSM-derived) for intraday vol; pull SVI parameters from /v1/adv_volatility if you need surface-fitted vol per strike.
Everything else - prices, greeks, OI, IV - is real, minute-accurate, and matches the live response shape exactly.
Coverage
SPY today: 2018-04-16 → 2026-04-02. Other symbols backfill on demand. Always check /v1/tickers first if you're scripting against a date range - the response tells you exactly which days are healthy and which have gaps:
curl -H "X-Api-Key: YOUR_API_KEY" \
"https://historical.flashalpha.com/v1/tickers?symbol=SPY"
Gates: Alpha tier ($1,499/mo) on every endpoint. Quota shared with the live API.
What This Replaces
If you've been doing any of these, the chain endpoint replaces them:
- Pulling raw historical options parquets from ThetaData and running your own BSM hydration → use
/v1/optionquote; greeks are already attached.
- Reconstructing chains from EOD snapshots and interpolating intraday → no longer necessary; minute resolution is real.
- Storing your own historical chain in a custom database → you can, but the API is faster than your DB for most ad-hoc research.
The cases where you'd still want raw historical data: backtests requiring tick-level fills, NBBO reconstruction, or volume analysis (where minute-aggregated isn't enough). For those, ThetaData remains the right tool. For everything else - analytics, research, ML features, dashboards - the chain endpoint collapses the pipeline.
Related Articles
Historical API · Alpha tier · from $1,199/mo
Replay any analytics endpoint at any minute since 2018
Same response shape as live, leak-free percentiles, 6.7B option rows for SPY, more symbols on demand.
View pricing →
Data freshness: intraday data through the previous trading day's close, refreshed by the daily pipeline run. Live coverage status at
/v1/tickers.
Upgrade to Alpha
API Spec