Alpaca options chain
two hosts, two envelopes, one chain
Pulling an options chain from Alpaca takes two calls to two different hosts, and both return their payload wrapped in an envelope. Neither mistake throws an error — you get an empty panel and no explanation. This page is the shape that actually works, and the three things that silently return nothing.
The two-host split is the first thing that bites
Alpaca serves accounts and orders from one host and market data from another. The contract list is a trading-API resource; the prices for those contracts are market data. So a chain always crosses hosts.
Account, order and position routes live on the trading host. Market data — stocks, options, bars, snapshots, quotes — lives on the data host. Mixing them returns 404 with a JSON body, which most HTTP clients will not throw on. A panel that quietly renders empty is the usual symptom.
Step 1 — list the contracts
GET https://api.alpaca.markets/v2/options/contracts
?underlying_symbols=SPY
&expiration_date_gte=2026-09-01
&expiration_date_lte=2026-09-30
&limit=1000
The response is not an array. It arrives as { "option_contracts": [ … ], "next_page_token": "…" }. Testing .length on that object gives undefined, so a loop guarded with if (!res.length) continue skips every symbol on every pass and never logs a thing.
const res = await fetch(url, { headers }).then(r => r.json());
const contracts = res?.option_contracts || (Array.isArray(res) ? res : []);
if (!contracts.length) return; // now this means what it says
Step 2 — price them
Snapshots come from the data host, take a comma-joined symbol list, and are themselves wrapped — in .snapshots, keyed by OCC symbol.
GET https://data.alpaca.markets/v1beta1/options/snapshots
?symbols=SPY260918C00600000,SPY260918P00600000
&feed=iex
sip is the consolidated tape and needs a paid entitlement; asking for it without one returns an error, not a downgrade. iex is free and is what a free plan should request. Pick one and use it everywhere — a chain priced on one venue and a chart drawn from another will disagree, and the difference looks like a bug in your maths.
Reading what comes back
Field names are not consistent across feeds and plans, which is why defensive reads matter more here than they normally would.
| What you want | Read this | Why it is not obvious |
|---|---|---|
| Bid / ask | latestQuote.bp / .ap | Zero-width or missing on illiquid strikes — treat as no market, not a price of 0. |
| Day volume | dailyBar.v | latestTrade.s is one print. Comparing it across polls measures nothing. |
| Implied vol | greeks.mid_iv, else impliedVolatility | Some feeds put IV at the top level and omit the greeks block entirely. |
| Open interest | from the contract record | OI is a daily figure and is not on the snapshot. |
A sanity check worth building in
Once you have a mid price, price the same contract yourself. Black-Scholes on the feed's own IV should land close to the feed's own mid. When it does not, the usual cause is a stale quote or a strike with no real market — both worth knowing before you trade off the number.
Price a contract on the feed's own implied volatility and compare it with the mid you were given. A large gap usually means a stale quote or a strike with no real market — not a broken model.
GreeksView does all of the above in your browser, on your own Alpaca keys — the chain, the Greeks, gamma exposure and payoff maths, with no key ever reaching our server.
Open a live chain freeQuestions
Why does my Alpaca options request return 404 when the URL looks right?
Almost always the host. Contracts are a trading-API resource (api.alpaca.markets); snapshots, bars and quotes are market data (data.alpaca.markets). The wrong pairing returns a 404 with a JSON body, which most clients do not raise as an exception — so it surfaces as an empty panel rather than an error.
Why is my contracts array empty when I know the contracts exist?
The response is an object, not an array. Read res.option_contracts. A guard written against res.length is testing undefined and will skip silently forever.
Do I need a paid Alpaca plan for options data?
Not for feed=iex, which is free and sufficient for chain structure, Greeks and gamma exposure. feed=sip is the consolidated tape and requires an entitlement; requesting it without one errors rather than falling back.
How far back does Alpaca options history go?
Roughly February 2024. Requests for earlier dates return nothing rather than an error, so code that assumes data exists will show a gap that looks like a failure.
Can I get expired contracts?
Yes — pass status=inactive. The default only returns tradable contracts, so any backtest or past-earnings analysis silently sees an empty list without it.
Point it at your own Alpaca account
Full option chains, live Greeks, gamma exposure and payoff modelling — computed in your browser on your own API keys. Paper keys work identically, and nothing you hold ever reaches our server.
Start free — no card required