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 of them return the same envelope except
chain, which returns { symbol, count, options[] } with camelCase rows
(ivError, bidIv, …).
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 those twelve years contain, probed field by field.
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 — 4/8/12 years by plan. | 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()
# rows are OBJECTS with named fields — read them by name
for contract in r["response"]:
for row in contract["data"]:
aggressive = row["price"] >= row["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 most history types, * means every expiration — which ones. |
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=*: a count, not a price band — strike_range=N keeps N strikes on each side of the at-the-money strike. 550,650 is rejected with 400. |
strike | 600 | In dollars, on the way in and on the way out — 600, never 600000. |
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 for ohlc, quote, trade_quote and greeks_history. Milliseconds are accepted (60000); defaults to one minute. |
Which types need a date — all of them that read history
Dates are not optional on the history types, and this is the one omission worth calling out:
they are not defaulted to today. Every date is YYYYMMDD
(2026-07-31 with dashes is accepted too), and date is shorthand for
start_date=end_date.
| Request types | Date parameters |
|---|---|
eod, oi, ohlc, quote, trade, trade_quote, greeks_history, trade_greeks | A window is required: start_date + end_date, or date for a single session. |
at_time | date and the instant, both required. Give the instant as time_of_day=hh:mm:ss (ET, e.g. 09:30:00) or as ivl in milliseconds into the day — 34200000 is 09:30:00 ET. It names the instant to read, not a bar size. Reads the last trade by default; kind=quote reads the quote instead. |
chain, greeks, root | None. These are live snapshots; a date on them is ignored. |
NDX and NDXP greeks (trade_greeks, greeks_history) are computed by us,
because no index level is published with them. The underlying is the forward implied by put-call parity at the
at-the-money strikes, interpolated to the second; implied vol and the greeks follow from the trade price or quote
mid. Each row says so with underlying_source: "implied_forward"; a row we could not price says
"unavailable" with iv_error: 1, and iv_error: 1 can also appear on an
implied_forward row when the fit behind it was poor — treat both as unpriced. Checked against the
vendor's own SPXW greeks, the median difference in implied vol is 0.6 vol points. Up to 31 sessions per request;
greeks_history accepts expiration=* here with max_dte (0–30), and
start_time/end_time are ignored — you get the whole session.
A request missing a date it needs now returns 400 bad_request with the
missing parameter named, and a pointer back to this page. It used to reach the vendor without one
and come back as a 502, which read like an outage on our side and was not.
The response envelope
One entry in response per contract, each carrying a contract block and
a data array. Rows are objects with named fields — there is no
header.format and no positional row. We forward the source body untouched rather
than re-mapping forty vendor columns into names of our own, so read every field by its
key and ignore key order.
{
"response": [
{
"contract": { "symbol": "SPY", "expiration": "2026-08-21",
"strike": 600.0, "right": "CALL" },
"data": [
{
"trade_timestamp": "2026-07-31T13:20:27.697", "sequence": -1534418721,
"size": 1, "condition": 138, "exchange": 6, "price": 147.06,
"quote_timestamp": "2026-07-31T13:20:27.292",
"bid_size": 39, "bid": 145.68, "bid_exchange": 5, "bid_condition": 50,
"ask_size": 18, "ask": 147.21, "ask_exchange": 9, "ask_condition": 50,
"ext_condition1": 255, "ext_condition2": 255, "ext_condition3": 255, "ext_condition4": 255
}
]
}
]
} Conventions worth knowing before your first parse
| Thing | What it actually is |
|---|---|
| Timestamps | ISO stamps — 2026-07-31T13:20:27.697 — in US/Eastern wall-clock time, with no offset on them. The field name depends on the type: trade_quote has trade_timestamp and quote_timestamp, eod has created and last_trade, greeks_history has timestamp. We pass them through rather than converting to UTC, because converting loses the session boundary you almost always want. Read them as Eastern; treating it as UTC puts every row hours into the future. |
strike in contract | Dollars, with three decimals — 600.000 is $600.00, not thousandths. Same as the query takes. |
expiration, right in contract | "2026-08-21" and "CALL"/"PUT" — dashed dates and spelled-out rights on the way out, even though the query accepts 20260821 and C. |
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 into one response array; a one-page answer has no header at all, a multi-page one carries header.pages_followed. We stop at 50 pages and then set header.truncated with a note — narrow the window if you see it. You will never see an internal URL. |
| Empty result | 404 no_data, not an empty array. A contract that did not trade in the window is a fact, not a failure — and it is distinct from an upstream outage, which is 502. |
| History clamp | A window older than your package reaches is clamped, not rejected, and the response says so in x-history-clamped and x-history-years. |
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, as a live snapshot, 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
The history types eod, oi, quote, trade,
trade_quote and trade_greeks accept expiration=*: one request
answers every contract of the root for the date window, instead of one request per expiration.
greeks_history (except on NDX/NDXP, which we compute ourselves — see above) and ohlc do not — ask them per expiration with
strike=* (every strike of that expiration in one request), and narrow the day with
start_time/end_time when you only need an instant. 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.
Contract lists: expirations and strikes
requiresOptions Core or above
Two metadata requests, open to every options package and not counted among the request types:
/v1/options/expirations?symbol=PLTR lists every expiration the root has had (listed and
expired), and /v1/options/strikes?symbol=PLTR&expiration=2026-10-16 the strikes of one
expiration.
A chain nobody streams yet
chain serves the live snapshot the gateway polls. A root that nobody streams is added
to the poll by the first request, which answers 503 chain_loading with
Retry-After: 60; the chain is there on the next poll cycle. A root that still has none
after five minutes answers 404 no_chain (no listed options). For a one-off board without
waiting, root (Options Pro) reads the whole root in a single request.
Every chain row carries the first-order greeks (delta, gamma, theta,
vega, rho) and the second-order ones vanna, charm,
vomma and veta, alongside iv and ivError — the same row on the
options WebSocket channel.
Live chains over WebSocket
Cadence, stated plainly: the live chain — REST and WebSocket alike — is a
full-chain snapshot (every strike, with greeks) refreshed about every 30
seconds per underlying. It is not per-second. If you need finer, you don't poll
the chain faster — you use the tape: the Flow/Pro history types carry every OPRA trade and
every NBBO quote with exchange timestamps, and Pro adds the intraday
greeks_history series.
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"] } The live tape: prints with the NBBO at them
The option_trades channel is every OPRA print as it happens, each one carrying the
national best bid and offer that stood at that moment. That pairing is what makes net flow
readable: a print at the ask was somebody paying up, a print at the bid was somebody hitting
it. A chain snapshot can only tell you volume went up, never which side lifted.
Subscribe by underlying root — the filter is applied here, so you receive only the roots you ask for. It is included from Options Flow upwards; Core streams the live chain but not the tape.
The whole tape: on Options Pro, "symbols": ["*"] subscribes to
every root at once, the full OPRA band. It counts as one symbol against your allowance, and you can
combine it with named roots. On Flow the wildcard is refused with a plan_required error
frame; the named roots in the same request still subscribe.
{ "op": "subscribe", "channel": "option_trades", "symbols": ["SPXW", "QQQ"] }
{ "op": "subscribe", "channel": "option_trades", "symbols": ["*"] } // Options Pro: every root
{
"type": "option_trade",
"symbol": "SPXW", "expiry": "2026-09-18", "strike": 6600, "right": "C",
"occ": "SPXW 260918C06600000",
"ts": 1789667131278, "price": 10.9, "size": 5,
"bid": 10.5, "bidSize": 12, "ask": 10.9, "askSize": 7, "quoteTs": 1789667131000,
"at": "ask",
"exchange": 65, "condition": 18, "seq": -563040482
} at is where the print landed: "ask", "bid",
"mid", or an empty string when no quote for that contract had arrived yet. We do
not translate it into "buy" or "sell" — at the mid there is no honest answer, and the
classification is yours to make. ts and quoteTs are exchange
milliseconds. Sizes and prices are the contract's, not per share.
SPX and SPXW alone print hundreds of times a second at the open, and a root like SPY can burst
into the thousands. Subscribe to the roots you actually read, and if your consumer falls
behind you will receive a warning frame with code option_tape_lagged
and the number of prints dropped — net flow computed across that gap is wrong and would
otherwise look fine.
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 — and how fine-grained
Two axes decide whether a plan fits a backtest: depth (how many years) and granularity (end-of-day rows vs tick-level intraday). And one rule that keeps coming up in support mail, so it gets its own sentence: nothing here is forward-only. Every history endpoint serves the archive from the first day of your subscription — a key created this morning queries all of its plan's years immediately.
| Package | History | Granularity |
|---|---|---|
| Options Core | 4 years | End-of-day only — one daily row per contract, no intraday timestamps |
| Options Flow | 8 years | Tick level — every trade, every NBBO quote, intraday bars, daily OI |
| Options Pro | 12 years | Tick level + intraday greeks_history, joins and whole-root snapshots |
That is an Options Pro workload, and here is the honest recipe: GEX at time T
= per-strike gamma at T × open interest. Open interest is a daily figure by nature
(published once per day, industry-wide — no vendor has intraday OI), so intraday GEX evolution
is computed as daily oi × the intraday greeks_history series as spot
and IV move — exactly how our own free GEX page
computes it live. Flow can get there too, but you would be inverting IV from raw NBBO quotes
across the whole chain yourself.
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.