"Historical implied volatility data" is a request that means different things to different people. A discretionary trader wants the IV term structure across DTE buckets. A quant building a vol model wants the smoothed surface. An ML engineer wants per-contract IV they can join to OI and greeks. A vol researcher wants the calibrated SVI parameters so they can refit themselves. The right historical IV API ships all four shapes, time-aligned, in the same response shape as the live equivalents.
FlashAlpha's Historical API does this. Four endpoints cover the IV stack, every one of them takes the same at parameter, every one of them works at minute resolution since 2018-04-16. This guide walks through each of them with example calls and the typical use case for each.
The Four Endpoints
| Endpoint | Returns | Best for |
/v1/optionquote/{ticker} | Per-contract BSM IV (with greeks & OI) | Per-strike IV at a moment, building features per contract |
/v1/volatility/{symbol} | RV ladder, IV-RV spreads, skew profiles, IV term structure | Term structure, skew analysis, full vol dashboard at a moment |
/v1/surface/{symbol} | 50×50 IV grid over (tenor, log-moneyness) | Surface plots, surface-shape features for ML, smooth IV at any moneyness |
/v1/adv_volatility/{symbol} | SVI params, forwards, total variance surface, arbitrage flags, variance-swap fair values | Vol-model research, SVI refit, variance-swap pricing |
Same at parameter. Same response shape as live. Pick the one that matches your job.
1. Per-Contract IV - /v1/optionquote
For per-strike IV at a moment, the contract endpoint is the one to use. Returns BSM-derived implied_vol for every contract, computed from the minute-level mid-quote and spot.
import httpx, pandas as pd
API_KEY = "..."
chain = httpx.get(
"https://historical.flashalpha.com/v1/optionquote/SPY",
params={"at": "2024-08-05T10:00:00", "expiry": "2024-08-09"},
headers={"X-Api-Key": API_KEY},
).json()
df = pd.DataFrame(chain)
df = df[(df["type"] == "C") & (df["mid"] > 0)]
df.set_index("strike")["implied_vol"].plot(
title="SPY 2024-08-09 calls - IV smile at 10:00 ET on the carry-trade unwind day"
)
One call, one expiry, full smile. implied_vol is BSM-derived from the mid; for SVI-smoothed vol per contract see the svi_vol caveat at the end of this article.
2. Vol Dashboard - /v1/volatility
For the full IV picture at a moment - ATM IV, realized vol ladder, IV-RV spread, skew profiles per expiry, IV term structure, IV dispersion - the volatility dashboard returns it all in one response. Same shape as the live /v1/volatility endpoint.
vol = httpx.get(
"https://historical.flashalpha.com/v1/volatility/SPY",
params={"at": "2024-08-05T10:00:00"},
headers={"X-Api-Key": API_KEY},
).json()
# IV term structure across all expiries
ts = pd.DataFrame(vol["iv_term_structure"])
ts.set_index("days_to_expiry")["iv"].plot(title="SPY ATM IV term structure - Aug 5, 2024 10:00 ET")
This is the call to make when you want the full vol dashboard at a single moment. Realized vols are computed from the QuestDB stocks table via SAMPLE BY 1d last-tick - so HV20, HV60, etc. are real, computed against actual historical closes available at at.
3. Smooth Surface - /v1/surface
For ML features, surface plots, or interpolated IV at any (tenor, moneyness), the surface endpoint returns a 50×50 grid:
surf = httpx.get(
"https://historical.flashalpha.com/v1/surface/SPY",
params={"at": "2024-08-05T10:00:00"},
headers={"X-Api-Key": API_KEY},
).json()
import numpy as np
iv_grid = np.array(surf["iv"]) # 50 x 50 grid
tenors = np.array(surf["tenors"]) # years to expiry
mny = np.array(surf["moneyness"]) # log-moneyness, -0.25 to +0.25
print(f"Grid shape: {iv_grid.shape}, slices used: {surf['slices_used']}")
The grid is built by the same calculator as live - bilinear interpolation from OTM contract IVs across tenor and log-moneyness. For sparse-history dates with too few liquid OTM contracts to fill the grid, the endpoint returns 404 insufficient_data rather than serving a degenerate surface.
This is your "ML feature store" call - flatten the grid into 2,500 features per timestamp and you have a dense surface representation the model can learn against. Combined with the dealer-positioning features from /v1/exposure/summary, that's enough surface area to train serious volatility models. Backtesting article shows the dealer-positioning side →
4. SVI Parameters - /v1/adv_volatility
For research that needs the calibrated parameters themselves - re-fitting your own model, computing variance swaps, generating arbitrage-free surfaces - the advanced volatility endpoint returns the SVI fit per expiry, plus forward prices, total-variance surface, arbitrage flags, and greek surfaces (vanna, charm, volga, speed):
adv = httpx.get(
"https://historical.flashalpha.com/v1/adv_volatility/SPY",
params={"at": "2024-08-05"}, # SVI is daily, defaults to 16:00 ET
headers={"X-Api-Key": API_KEY},
).json()
svi = pd.DataFrame(adv["svi_parameters"])
print(svi[["expiry", "a", "b", "rho", "m", "sigma", "atm_iv"]])
SVI parameters are EOD-stamped - one fit per trading day, applied across the day. That matches reality: SVI doesn't update intraday in any meaningful way; greeks and quotes do. If you need intraday vol changes, use the surface or per-contract endpoints; if you need calibrated parameters for re-fitting or pricing, use this one.
The arbitrage_flags array tells you, per expiry, whether the fit had any butterfly or calendar arbitrage flagged during calibration. For research that depends on arbitrage-free surfaces, filter on this. SVI methodology background →
Building an IV Term-Structure History (4 Years)
The most common "historical IV" research question: how did IV term structure evolve over time? Pull the volatility dashboard daily across your window and extract the term structure.
from tqdm import tqdm
dates = pd.bdate_range("2022-01-03", "2025-12-31")
rows = []
with httpx.Client(headers={"X-Api-Key": API_KEY}, timeout=30) as c:
for d in tqdm(dates):
r = c.get(
"https://historical.flashalpha.com/v1/volatility/SPY",
params={"at": d.strftime("%Y-%m-%d")},
)
if r.status_code != 200: continue
ts = r.json().get("iv_term_structure", [])
# Each entry has days_to_expiry + iv (ATM IV at that tenor)
for slot in ts:
rows.append({"date": d,
"dte": slot["days_to_expiry"],
"iv": slot["iv"]})
iv = pd.DataFrame(rows)
# Bucket DTEs to standard tenors so plots align day to day
iv["bucket"] = pd.cut(iv["dte"], bins=[0, 14, 45, 120, 365],
labels=["~7d", "~30d", "~90d", "long"])
pivot = iv.groupby(["date", "bucket"])["iv"].mean().unstack("bucket")
pivot[["~7d", "~30d", "~90d"]].plot(
title="SPY ATM IV by tenor bucket - 2022 to 2025", figsize=(12, 5))
About 1,000 trading days, one call per day. The resulting DataFrame is the canonical "IV term structure over time" dataset - feed it into vol-targeting strategies, regime classifiers, or just use it to plot when contango broke into backwardation across the test window.
The svi_vol Caveat
One field in /v1/optionquote is intentionally null in historical mode: svi_vol (SVI-smoothed vol per contract) returns null with "svi_vol_gated": "backtest_mode". The SVI parameters themselves are fitted and available via /v1/adv_volatility at the expiry level - but the per-contract derivation isn't plumbed in the historical service yet.
Workaround: if you need SVI-smoothed vol per contract for a specific moment, pull the SVI parameters via /v1/adv_volatility and evaluate the SVI formula yourself per strike. For most research, the BSM-derived implied_vol on each contract is what you want anyway - it's the "real" IV implied by the market's mid quote, not a smoothed value.
Coverage and Caveats
- Granularity: per-contract IV and surface are minute-level; SVI parameters are EOD.
- Coverage: SPY 2018-04-16 → 2026-04-02. More symbols on demand.
- Tier: Alpha ($1,499/mo). Quota shared with live API.
- Surface failures:
/v1/surface returns 404 insufficient_data for dates with too few liquid OTM contracts. SPY post-2018 rarely hits this; older history or low-volume symbols can.
What This Replaces
If you've been doing any of these for historical IV research, the four endpoints replace them:
- Pulling raw historical chains and inverting BSM yourself for IV →
/v1/optionquote ships implied_vol attached.
- Computing your own term-structure aggregations from EOD chains →
/v1/volatility ships the term structure pre-computed.
- Fitting your own SVI parameters daily →
/v1/adv_volatility ships them, with arbitrage flags.
- Storing your own surface grids per minute →
/v1/surface ships a fresh 50×50 per call.
Where you'd still build it yourself: bespoke surface methodologies (gSVI, SSVI, jump-diffusion), or non-standard moneyness/tenor grids. For everything else, the bought-not-built path is faster.
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