REST API
Query the data directly.
Use HTTPS requests from your own application, notebook or data pipeline. The same account permissions and usage limits apply across REST, Python and MCP.
Connection and authentication
Base URL:
https://optionspace.ai/api/v1/data
Send your key in the Authorization: Bearer … header. An x-api-key header is also accepted. Never put the key into the URL. A website login session is not a substitute for the API key on these endpoints.
The examples below prompt for the key in the terminal using Python's hidden input. They do not save it. For a saved credential shared with Python and MCP, use the package login flow.
Make your first request
Save this as first_request.py, then run it with uv run --with requests first_request.py (any system, any folder; see installing uv). Without uv: install requests (python3 -m pip install requests, Windows py -m pip install requests) and run python3 first_request.py (Windows py first_request.py):
from getpass import getpass
import requests
key = getpass("OptionSpace API key (input hidden): ").strip()
base = "https://optionspace.ai/api/v1/data"
headers = {"Authorization": f"Bearer {key}"}
response = requests.get(
f"{base}/entitlements",
headers=headers,
timeout=30,
allow_redirects=False,
)
response.raise_for_status()
print(response.json())
Paste the key (Ctrl+V on Windows, Command+V on macOS, Ctrl+Shift+V in most Linux terminals) and press Enter when the prompt appears. No characters will appear while pasting. A successful response is HTTP 200 with your account's access and usage information.
First call with curl
To check the API from the terminal without Python, this reads the key at a hidden prompt, makes two calls and clears the key again. macOS or Linux, in two parts. Pasted as one block, the first line would read the next line as your key.
Run this line on its own first, then paste your key and press Enter (nothing appears as you paste, which is normal):
printf 'OptionSpace API key (hidden): '; read -rs OPTIONSPACE_API_KEY; echo; export OPTIONSPACE_API_KEY
Then run the calls:
curl -sS -H "Authorization: Bearer $OPTIONSPACE_API_KEY" \
https://optionspace.ai/api/v1/data/entitlements
curl -sS -H "Authorization: Bearer $OPTIONSPACE_API_KEY" \
"https://optionspace.ai/api/v1/data/smile?symbol=SPY&data_date=2025-04-10&expiration=2025-04-17&snapshot=EOD" \
| head -c 800
unset OPTIONSPACE_API_KEY
Windows PowerShell, also in two parts (the prompt shows asterisks as you paste; curl.exe comes with Windows 10 and 11):
Run this line on its own first, then paste your key and press Enter:
$key = [Net.NetworkCredential]::new('', (Read-Host 'OptionSpace API key' -AsSecureString)).Password
Then run the calls:
curl.exe -sS -H "Authorization: Bearer $key" https://optionspace.ai/api/v1/data/entitlements
$r = curl.exe -sS -H "Authorization: Bearer $key" "https://optionspace.ai/api/v1/data/smile?symbol=SPY&data_date=2025-04-10&expiration=2025-04-17&snapshot=EOD"
$r.Substring(0, [Math]::Min(800, $r.Length))
Remove-Variable key, r
The first call prints your plan, limits and usage as JSON; the second prints the start of a smile. The key stays in that terminal window only, and the last line clears it.
Discover, then fetch
Continue in the same Python file, using the base and headers defined above. The sample dates require permission for April 2025.
response = requests.get(
f"{base}/expirations",
headers=headers,
params={"symbol": "SPY", "data_date": "2025-04-10",
"snapshot": "EOD"},
timeout=30, allow_redirects=False,
)
response.raise_for_status()
print(response.json())
response = requests.get(
f"{base}/smile",
headers=headers,
params={"symbol": "SPY", "data_date": "2025-04-10",
"expiration": "2025-04-17", "snapshot": "EOD"},
timeout=30, allow_redirects=False,
)
response.raise_for_status()
print(response.json())
Use /symbols, /snapshots and /expirations to check available coordinates before automating larger queries. Do not assume every symbol, expiry or minute exists or is included in your grant.
Endpoint map
All routes below use GET beneath the base URL.
| Route | Purpose | Typical selection |
|---|---|---|
/entitlements |
Access, limits and current usage | Authenticated key |
/symbols |
Symbol coverage | Authenticated key |
/snapshots |
Available session times for your key | Symbol and date |
/expirations |
Listed expiries and roots | Symbol, date, snapshot |
/smile |
Fitted data for one expiry | Symbol, date, expiry, snapshot |
/chain |
Options across expiries at one snapshot | Symbol, date, snapshot |
/atm-timeseries |
One expiry's intraday ATM path | Symbol, date, expiry |
/tenor-history |
Supported constant-maturity history | Symbol, tenor, date range |
/rates |
Implied rates | Date range |
/surface |
Legacy end-of-day term summary | Symbol and date |
/expiry-metrics |
Listed-expiry metrics and optional previous-session comparison | Symbol, date, expiry, snapshot |
/exposure-distribution |
Fitted-IV distribution for a stored-delta-selected exposure | Symbol, date, explicit minute, side, delta, DTE, lookback |
/clean-atm-history |
Constant-maturity ATM volatility with and without scheduled events | Symbol, date range, tenor |
/earnings-history |
Past earnings: priced event move, realized move and the run-up before each report | Symbol, as-of date, number of reports |
/earnings-backtest |
A bounded earnings put-spread simulation with fills, fees, P&L and risk | Symbol, as-of date, number of reports, entry and exit sessions |
/iv-rv |
Implied vs realized volatility at 1 to 30 days, and their gap | Symbol, tenor, date range |
/event-outcomes |
Earnings, Fed, CPI and jobs-report days: the move priced vs the move that happened | Symbol, date range, event type |
/event-runup |
The move priced for each event on the sessions before it | Symbol, date range, event type |
/volume-summary |
Daily volume and open interest for a symbol, calls vs puts | Symbol, date range |
/contract-volume |
One option's daily volume and open interest | Symbol, expiry, strike, call or put, date range |
Every route's parameters, whether each is required, its default and allowed values, and what the route costs: see the reference.
SPX and SPXW
SPX options list under two roots: SPX, the AM-settled standard monthlies (and some long-dated expiries), and SPXW, the PM-settled expiries on most other days. You do not need to pass root:
- On a date only one root lists, that root is served.
- On the third-Friday monthlies, where both list the same date,
SPXW(PM-settled) is served unless you passroot=SPX. /surfacewithoutroottakes each expiry from the root that lists it,SPXWwhere both do; passrootfor one root only./chainwithoutrootreturns both roots' rows; each row'ssymbolis its root.
Every response names the root it served (root, or root on each expiry or row). Passing root always selects that root; a root the symbol does not list is refused with a 400 that lists the valid ones.
/surface is a separate legacy end-of-day summary; do not assume it is identical to /term-structure. /term-structure returns, in one request, the same values /smile gives for each listed expiry; the Python term_structure() method and MCP term comparison use it.
For exact Python argument names and defaults, use the reference. Every response field is described in the reference.
Research routes
These routes answer a whole research question in one request. A long one becomes a background job: see credits and long research calls. They work on completed sessions only: the window must end before today (US Eastern time). Symbols that list under several roots (for example SPX, which lists as SPX and SPXW) are not accepted here, except by /expiry-metrics, which picks the root as described in SPX and SPXW. Each response carries notes that state what the numbers are and are not, and a status on every row or event.
Clean ATM history: /clean-atm-history
Constant-maturity ATM volatility through time, with scheduled events removed (clean_iv) and without (dirty_iv). For a stock, the clean value removes the next scheduled earnings report when it falls within 90 days; for an index or ETF, scheduled macro releases are removed. Where an event cannot be removed, clean_iv repeats the unadjusted value, clean_iv_se is empty and status says why. The window may span at most three years and must end before today.
Parameters and every response field.
Earnings history: /earnings-history
The most recent earnings reports before an as-of date: the move the options priced, the move that followed, and how the priced move built up beforehand. The fields are the same as the event lookups': implied_move (see implied move and event volatility), actual_return, actual_move, return_overnight, return_intraday, and run-ups by sessions_to_event. The route reads up to six years of history before as_of; if your access is limited to a sample window, that whole lookback must fall inside it.
Parameters and every response field.
Earnings backtest: /earnings-backtest
For each recent report, sells a stored-delta put spread a set number of sessions before the report and closes it after, then returns the trade, the fills and the position's path. The spread is short a put near 25 delta and long a put near 10 delta, in the first standard monthly expiry after the report, one spread per event. The response states these rules under assumptions. P&L is in USD for one spread. Quote-based fills (quote_side, midpoint) need an as-of date whose quotes have been released (see quote availability); use theoretical otherwise. The same six-year lookback rule applies as for /earnings-history.
Parameters and every response field.
Volatility, events and volume
These are lookups: one request a call, and rows count toward your daily row limit. They cover every covered symbol your plan includes, over the dates your plan covers (on the Free plan, the sample month).
Implied vs realized volatility: /iv-rv
At-the-money implied volatility, realized volatility and the gap between them (vrp, implied minus realized) at 1, 2, 3, 5, 7, 10, 14, 20 and 30 days, through time. Pick one tenor_days per call: the implied side looks that many calendar days ahead, the realized side back over that many trading sessions. Like /tenor-history, leave snapshot out for every snapshot your plan covers, or pass EOD or HH:MM. The short tenors are often empty for symbols without daily expiries: a 1-day value needs a listed expiry within a day (notes says so when a row is empty). Volatilities are decimals (0.20 = 20%).
Parameters and every response field.
Event outcomes: /event-outcomes
One row per event: the move the options priced at the last close before it, the move that followed (actual_return, signed; actual_move, its size), how that move split between the overnight gap and the trading session, and the 30-day ATM volatility before and after. Event types: earnings (single stocks), and fomc (Fed rate decisions), cpi (consumer price index releases) and nfp (the monthly jobs report) for index and ETF symbols. Leave event_type out for all of them. Moves are decimals of the share price (0.05 = 5%).
implied_move is empty for an event the options priced no measurable extra move for; this is common for CPI and jobs-report days.
Parameters and every response field.
Event run-ups: /event-runup
For each event dated in your range, the move priced for it on each session before it, with that session's 30-day ATM volatility. sessions_to_event counts trading sessions: 0 is the last close before the announcement (the event day itself for an announcement after the close, the session before for one before the open or during the session), -1 the session before that. Exchange holidays are not sessions. An upcoming event appears with the sessions up to the latest one stored.
Parameters and every response field.
Implied move and event volatility
implied_move is the average size of the move the options priced for an event, up or down, as a fraction of the price. It is the same measure in /event-outcomes, /event-runup and /earnings-history.
It comes from the event's volatility: one standard deviation of the move the options priced for that event alone, not annualized. For a normally distributed move, the average size is √(2/π) ≈ 0.80 times one standard deviation:
implied_move = 0.80 × event volatility, and event volatility = 1.25 × implied_move
NVDA's report on 26 August 2026, after the close: implied_move was 0.0544 (5.44%), so the event volatility was about 6.8%. The stock then rose 8.60% from that close to the next one (actual_return 0.0860: 6.71% overnight, then 1.77% during the session), more than the options priced.
Comparing actual_move with implied_move compares like with like: both are the size of the move. actual_minus_implied on /event-outcomes is that difference.
Volume and open interest: /volume-summary, /contract-volume and /chain
Volume and open interest are exchange market data, served for past sessions only: a session's figures are available from the next session's 09:30 ET open, the same rule as quotes. A request that reaches a session not yet available is refused with 403 (reason not_yet_historical, which names the first unavailable date).
/volume-summary: per session, the official end-of-day volume for calls, puts and both, open interest, and the put/call ratios./contract-volume: one option's official daily volume and open interest across a date range. For SPX monthlies listed under bothSPXandSPXW,SPXWis served unless you passroot=SPX./chain: addvolumeand/oropen_interesttocolumnsto get them on each row.volumethere is the contracts traded so far that session up to the snapshot minute, so it grows through the day; the official end-of-day figure is in/contract-volume. They are not in the default columns. On a large chain they make the call slower; filtering byexpirationkeeps it fast.
Open interest on a session's row is the figure published before that session opened, which is the open interest at the end of the previous session.
Parameters for /volume-summary and for /contract-volume.
Credits
Your plan includes a daily allowance of API credits. It resets at midnight UTC and is shared by every key on your account; signed-in use of the website's dashboards draws on the same allowance. Rows a day and calls at a time are separate limits. Your plan's allowance is on the pricing page and in limits on /entitlements. The allowance is for your own research. Plans are not for making a copy of the data (Terms, Section 5).
What each call costs is in the price list: the prices field of /entitlements (client.prices() in Python), with its version and the date it took effect. Prices may change; a change is announced when it is made.
Research calls are estimated first. Before a research call runs, the server estimates it from its parameters: the route, daily or intraday data, and how much history it reads. The estimate sets the call's cap, the most it can cost. The cap is held against your allowance while the call runs, and the unused part is released when it finishes.
estimate=trueon any research route returnscredits_estimateandcredits_capwithout running the call.max_credits=Nrefuses the call with412if its cap is aboveN. The call is not run.
Time series are counted in periods. One call of /tenor-history, /iv-rv or /rates covers at most 10,000 periods. A period is one snapshot of one session (and one root, for a symbol that lists under several). The coarser the sampling, the longer the range one call can cover:
| Sampling | Periods a session | One call covers about |
|---|---|---|
End of day (sampling=eod, snapshot=EOD or one HH:MM) |
1 | 40 years |
sampling=30m |
14 | 2.8 years |
sampling=5m |
78 | 6 months |
sampling=1m |
386 | 5 weeks |
A longer range is the same data in more calls. A call over the limit returns 400 with reason too_many_periods, the number of periods asked for, and suggested_end, the last session that fits; it is not run. The limits and periods per session are in prices.periods_per_call on /entitlements.
Every response says what it cost in the X-Credits-Charged header, with X-Credits-Remaining (what is left today), and for research X-Credits-Estimate and X-Credits-Cap. Research responses also carry a credits object: charged, estimate, cap and work_seconds.
Long research calls
A research call that is still working after about 20 seconds answers 202 Accepted instead of waiting, with a job_id, a poll_url and poll_after_seconds. Collect the answer from GET /api/v1/data/jobs/{job_id} with a key from the same account. Its status is queued, running, done (with result: the body the call would have returned) or failed (with error: the status and message the call would have returned).
- Results are kept for one hour after the job finishes. The job's
creditsare set when it finishes. - One research call runs at a time per account and up to two more can wait behind it. A fourth is refused with
429(reasonresearch_queue). DELETE /api/v1/data/jobs/{job_id}cancels a job that has not started. A running job cannot be stopped.- The Python package and the MCP tools wait for the job for you, polling at a growing interval.
Interpret the response
| Convention | What it means for your query |
|---|---|
| Dates | YYYY-MM-DD; use trading sessions where applicable |
| Times | HH:MM in US Eastern market time, not UTC |
EOD |
Resolves to an allowed snapshot at or before the official session close; inspect the returned time |
| Expiration vs tenor | A listed expiration is a contract date; a tenor is a constant-maturity target. They are different queries |
| Product symbol vs root | A product can contain multiple listed roots. Keep the returned root; see SPX and SPXW for which one is served when you do not pass root |
| Volatility | Decimal values in API data: 0.20 is 20% volatility. Charts may display percentages or vol points |
| Quote availability | Quote fields for a session are served from the next session's 09:30 ET open; before then they are absent. See quote availability |
| Provenance | Preserve snapshot identity and data-source information when saving or comparing observations |
Read the endpoint's returned status, notes and coverage. An empty selection or insufficient historical sample should not be silently converted into a zero-valued statistic. The percentile is bounded from 0 to 100; a robust z-score is not bounded and is not a normal-tail probability.
Field reference
Every field of every response is listed route by route in the reference: its type, unit or format, when it can be empty, what it means and an example. Field names are the same in REST responses, Python DataFrames and MCP results. Volatilities are decimals (0.20 = 20%), prices are USD per share and times are US Eastern.
Fit quality
Each fitted expiry carries a quality block: status, share_inside, miss_spreads, miss_usd, n_checked and codes, the reasons a fit may be less reliable. What each field and each code means: fit quality.
Quotes
Quote fields for a session are served from the next session's 09:30 ET open; until then they are absent from the response, not null. The full rule, and how to read quote_state: quote availability.
Errors, retries and usage
| Status | Next action |
|---|---|
401 |
Check the key, authentication header and whether the key has been revoked |
403 |
Check symbol/date/frequency/field permissions; repeating the same request does not add access |
404 |
With reason symbol_not_covered: the symbol is not in OptionSpace; /symbols lists the covered ones. With reason no_fittable_chain: the market had too few quotes with a bid on that day for a curve, so there is nothing to serve for it. Otherwise, check the selected data coordinates and availability |
400 / 422 |
Read the validation message and correct the parameters. A 422 lists each missing or mistyped parameter |
412 |
A research call's cap is above the max_credits you passed; it was not run. Raise max_credits or narrow the query |
429 |
Read the reason (for example daily_credits, with cost: what this call would hold), usage, limits and Retry-After; reduce the request rate or wait for the limit |
202 |
The research call became a background job; collect it from poll_url (see long research calls) |
5xx or timeout |
Retry with a bounded delay. After a timeout, check usage (X-Credits-Remaining, or /entitlements) before assuming the original request did not run |
Usage is pooled across the organization. Multiple keys do not create independent allowances. Discovery, multi-expiry studies and agent workflows can make several requests; inspect current usage before a large pull, and ask a research route for its estimate=true before a large one.
If you need help, send the endpoint, non-secret parameters, HTTP status and error message to [email protected]. Do not send your key or authorization header.