DOCS
Options data
Full OPRA options data over REST: 12 request types from live chains to trades joined with the NBBO of that millisecond, 12 years deep.
Every US listed option, from OPRA: every trade, every NBBO quote, greeks, open interest and
the joins between them. 12 request types, all under
/v1/options/<type>, all returning the same envelope.
The previous provider is gone. If you were reading the options WebSocket channel
for chains and greeks, that still works and is described below — but
everything historical, every trade and every quote now comes from these REST types instead,
with twelve years of history behind them rather than a few months.
What each package unlocks
The tiers are not three sizes of the same thing. Each unlocks the first N request types in the canonical order below, and the order is the argument: the surface, then the tape, then the joins.
The surface — what the chain looks like
| Type | Endpoint | What it returns | Package |
|---|---|---|---|
chain | /v1/options/chain | Live chain — bid/ask/last, open interest and volume per strike. | Options Core |
greeks | /v1/options/greeks | Live greeks and implied vol per strike, for one expiration. | Options Core |
eod | /v1/options/eod | Daily close per contract, twelve years back. | Options Core |
The tape — what actually printed
| Type | Endpoint | What it returns | Package |
|---|---|---|---|
ohlc | /v1/options/ohlc | OHLC bars per contract, down to tick interval. | Options Flow |
oi | /v1/options/oi | Open interest per contract per day — the opening-vs-closing input. | Options Flow |
quote | /v1/options/quote | Every NBBO quote reported by OPRA, with size and exchange. | Options Flow |
trade | /v1/options/trade | Every option trade reported by OPRA, with size and condition. | Options Flow |
The joins — what a print meant
| Type | Endpoint | What it returns | Package |
|---|---|---|---|
trade_quote | /v1/options/trade_quote | Each trade paired with the NBBO standing at that millisecond — the flow primitive. | Options Pro |
greeks_history | /v1/options/greeks_history | Historical greeks and IV as a time series. | Options Pro |
trade_greeks | /v1/options/trade_greeks | Every trade with the greeks as they stood at trade time. | Options Pro |
at_time | /v1/options/at_time | The exact trade or quote in force at a given timestamp. | Options Pro |
root | /v1/options/root | Whole-root bulk snapshot — every expiration in a single call. | Options Pro |
A request type above your package returns 403 with
request_type_not_in_plan, the package that would unlock it, and the list your key
does have. It never returns a thinner version of the answer — a silently reduced result set is
the worst possible failure for a backtest.
Making a call
requiresOptions Core or above
curl "https://api.tick-stream.xyz/v1/options/trade_quote?symbol=SPY&exp=2026-08-21&strike=600&right=C&date=2026-07-31" \
-H "Authorization: Bearer sk_live_…"import requests
r = requests.get(
"https://api.tick-stream.xyz/v1/options/trade_quote",
params={"symbol": "SPY", "exp": "2026-08-21",
"strike": 600, "right": "C", "date": "2026-07-31"},
headers={"Authorization": "Bearer sk_live_…"},
).json()
# columns arrive by NAME in the header — never index by position
cols = {c: i for i, c in enumerate(r["header"]["format"])}
for row in r["response"][0]["ticks"]:
aggressive = row[cols["price"]] >= row[cols["ask"]]const u = new URL("https://api.tick-stream.xyz/v1/options/trade_quote");
u.searchParams.set("symbol", "SPY");
u.searchParams.set("exp", "2026-08-21");
u.searchParams.set("strike", "600");
u.searchParams.set("right", "C");
u.searchParams.set("date", "2026-07-31");
const r = await fetch(u, { headers: { Authorization: "Bearer sk_live_…" } }); Parameters
| Parameter | Example | Notes |
|---|---|---|
symbol | SPY | The root. underlying and root are accepted as aliases. |
exp | 2026-08-21 | Expiration. expiration is accepted; dashes optional. On the history types, * means every expiration — see bulk history. |
max_dte | 30 | With exp=*: only contracts within N calendar days of expiry, measured on each session in the window. |
strike_range | 550,650 | With exp=*: clip the strike ladder instead of taking all of it. |
strike | 600 | In dollars. We convert to the thousandths the vendor wants — you never see 600000 on the way in. |
right | C / P | Call or put. |
date | 2026-07-31 | One session. Expands to start_date+end_date internally. |
start_date, end_date | 2026-07-01 | A range instead of one session. |
interval | 60000 | Bar interval in ms, for ohlc, quote, greeks and implied_volatility. Defaults to one minute. |
The response envelope
Rows are arrays, and the column names are in header.format. Read them by
name. Column order is the vendor's and is not part of our contract — code that indexes
by position will break on a day you are not watching.
{
"header": {
"format": ["ms_of_day", "sequence", "size", "condition",
"price", "bid_size", "bid", "ask_size", "ask", "date"],
"next_page": "null"
},
"response": [
{
"contract": { "root": "SPY", "expiration": 20260821, "strike": 600000, "right": "C" },
"ticks": [
[51293846, 88213, 250, 18, 4.35, 88, 4.30, 120, 4.35, 20260731]
]
}
]
} Conventions worth knowing before your first parse
| Thing | What it actually is |
|---|---|
ms_of_day + date | Milliseconds since midnight and YYYYMMDD, both in US/Eastern. We pass them through rather than converting to UTC, because converting loses the session boundary you almost always want. |
strike in contract | Thousandths of a dollar — 600000 is $600.00. Only on the way out; the query takes dollars. |
iv_error | 0.0 means the solver converged. 100.0 is the failed-solve sentinel — drop those rows rather than trusting the IV beside them. |
| Paging | Handled for you. We follow next_page internally and concatenate, so a multi-page answer arrives as one response. You will never see an internal URL. |
| Empty result | An empty ticks array, not an error. A contract that did not trade that session is a fact, not a failure. |
trade_quote — the one worth the upgrade
requiresOptions Pro or above
Every trade paired with the NBBO that stood in that exact millisecond. Without the pairing a print is a number; with it, it is an aggressive buy or a passive fill:
price >= ask → lifted the offer
price <= bid → hit the bid
size > ask_size → took more than was displayed That last line is why the join matters more than the trade feed alone. A 250-lot into a 120-lot offer is a different event from a 250-lot into a 5,000-lot offer, and the trade record on its own cannot tell you which one happened.
Whole-root snapshots
requiresOptions Pro or above
/v1/options/root returns every expiration of a root in one call, rather than one
request per contract. It is the difference between a chain sweep that takes a second and one that
takes four hundred requests.
Bulk history — the whole chain in one request
requiresOptions Core or above
Every history type accepts expiration=*: one request answers every
contract of the root for the date window, instead of one request per expiration. This is the
shape to use for backfills — a month of QQQ end-of-day (60,260 contracts) is a single call
that returns in about twenty seconds:
/v1/options/eod?symbol=QQQ&expiration=*&start_date=2019-03-01&end_date=2019-03-31
/v1/options/oi?symbol=QQQ&expiration=*&start_date=2019-03-01&end_date=2019-03-31
/v1/options/trade_quote?symbol=QQQ&expiration=*&max_dte=7&date=2024-03-15 max_dte and strike_range clip the wildcard: the third example is the
full front-week tape of a session — every trade with its NBBO — without pulling the LEAPS
ladder along with it. Pulling seven years this way is a few hundred month-sized requests, not
hundreds of thousands of per-contract ones.
The EOD report is generated at 17:15 ET and open interest arrives the next morning — the
running session's report does not exist yet. If your window includes today, we clamp it to
yesterday and say so in an x-end-clamped: running-day response header. Today's
chain is the live side's job: chain, root or the WebSocket below.
Live chains over WebSocket
requiresOptions Core or above
The options channel still streams live chains with greeks by underlying — unchanged
by the provider switch. Subscribe the same way as any other channel:
{ "op": "subscribe", "channel": "options", "symbols": ["SPY", "QQQ"] }
Deep OTM and deep ITM strikes routinely return zero greeks, and occasionally an absurd IV.
That is the pricing model failing on an illiquid strike, not missing data, and we pass it
through unchanged rather than inventing a value. Filter on iv_error and on quote
size before you aggregate — those rows are exactly the ones that would otherwise dominate a
GEX or vol-surface sum.
How far back
| Package | History | Tick level |
|---|---|---|
| Options Core | 4 years | — |
| Options Flow | 8 years | yes |
| Options Pro | 12 years | yes |
The contract universe itself reaches back to 2012-06-01; a request older than your package's window is clamped to it rather than rejected. Verified at the far end on 2026-08-03: SPY 132C expiring 2012-06-16, session 2012-06-11 — 2,592 prints, 2,137 distinct millisecond stamps, full OPRA fields with the NBBO beside them.
Viewing this data as a professional is an OPRA licence question, not a plan question, and no self-service package covers it. The arithmetic is public — work out your number.