tickstreamdocs

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, …).

this replaced the old options feed on 2026-08-02

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

TypeEndpointWhat it returnsPackage
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

TypeEndpointWhat it returnsPackage
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

TypeEndpointWhat it returnsPackage
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
refused, not truncated

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

ParameterExampleNotes
symbolSPYThe root. underlying and root are accepted as aliases.
exp2026-08-21Expiration. expiration is accepted; dashes optional. On most history types, * means every expiration — which ones.
max_dte30With exp=*: only contracts within N calendar days of expiry, measured on each session in the window.
strike_range550,650With 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.
strike600In dollars, on the way in and on the way out — 600, never 600000.
rightC / PCall or put.
date2026-07-31One session. Expands to start_date+end_date internally.
start_date, end_date2026-07-01A range instead of one session.
interval60000Bar 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 typesDate 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

ThingWhat it actually is
TimestampsISO 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 contractDollars, 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_error0.0 means the solver converged. 100.0 is the failed-solve sentinel — drop those rows rather than trusting the IV beside them.
PagingHandled 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 result404 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 clampA 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.

eod and oi end yesterday, by design

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.

it is the whole tape

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.

greeks at the edges

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.

PackageHistoryGranularity
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
backtesting intraday GEX / dealer positioning?

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.

professional display use

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.