HomeGuidesFor Alpaca › Alpaca Options Chain

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.

Calls per chain
2
contracts, then snapshots
Contracts per page
1,000
hard limit, then paginate
Wrong host returns
404
with a JSON body, not a throw
Options history from
Feb 2024
nothing earlier exists

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.

Trading hostapi.alpaca.marketsData hostdata.alpaca.markets/v2/options/contractswhich strikes exist/v1beta1/options/snapshotswhat they cost404 — silent
Contracts come from the trading host, prices from the data host. Sending either to the other returns 404.
The failure mode is silence

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
feed=iex vs feed=sip

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 wantRead thisWhy it is not obvious
Bid / asklatestQuote.bp / .apZero-width or missing on illiquid strikes — treat as no market, not a price of 0.
Day volumedailyBar.vlatestTrade.s is one print. Comparing it across polls measures nothing.
Implied volgreeks.mid_iv, else impliedVolatilitySome feeds put IV at the top level and omit the greeks block entirely.
Open interestfrom the contract recordOI is a daily figure and is not on the snapshot.
Where each number actually lives on a 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.

Feed mid vs Black-Scholes

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.

Model price
Black-Scholes on the feed's IV
Gap to mid
model minus feed

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 free

Questions

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
Already have an account? Sign in. Free tier has no time limit.