A request to the options contracts endpoint, /v3/reference/options/contracts, for a past expiration date comes back empty, and the two parameters that bring the contracts back answer different questions: expired=true lists the contracts that had expired by today, and as_of lists the chain as it stood on a day you choose. The as_of date decides which contracts you get: the same SPY expiration, 20 March 2026, returns 384 contracts as of 2 February 2026 and 528 as of 13 March 2026. All 384 are among the 528, and the other 144 were listed in between. So as_of gives you the chain as it stood on that day, not today's chain moved back in time.
Applies to
- Plans: every Options plan includes the options contracts endpoint. How far back each plan can reach is in the Plan History table of the All Contracts documentation.
- Endpoints: GET /v3/reference/options/contracts.
- Asset classes: options on US stocks, ETFs and indices.
How to list the contracts for a past expiration
To list every contract for a past expiration on the options contracts endpoint, pass expired=true, or set as_of to the expiration date. An earlier as_of returns the chain as it stood on that day, which leaves out contracts listed after it. The four calls below all ask for SPY contracts expiring on 20 March 2026.
No expired and no as_of. The endpoint checks the expiration against today:
curl -X GET "https://api.massive.com/v3/reference/options/contracts?underlying_ticker=SPY&expiration_date=2026-03-20&limit=1000&apiKey=YOUR_API_KEY"
Response
{
"results": [],
"status": "OK"
}
An empty list with status OK, not an error: every one of those contracts had expired before today, and by default the endpoint lists only contracts that had not.
With expired=true. The endpoint lists the contracts that had expired by today:
curl -X GET "https://api.massive.com/v3/reference/options/contracts?underlying_ticker=SPY&expiration_date=2026-03-20&expired=true&limit=1000&apiKey=YOUR_API_KEY"
Response, trimmed to the first two contracts:
{
"results": [
{
"cfi": "OCASPS",
"contract_type": "call",
"exercise_style": "american",
"expiration_date": "2026-03-20",
"primary_exchange": "BATO",
"shares_per_contract": 100,
"strike_price": 245,
"ticker": "O:SPY260320C00245000",
"underlying_ticker": "SPY"
},
{
"strike_price": 250,
"ticker": "O:SPY260320C00250000"
}
],
"status": "OK"
}
The full response holds 528 contracts, 264 calls and 264 puts with strikes from 245 to 1,100, and has no next_url, so one call returned the whole expiration.
With as_of before the expiration. The endpoint lists the contracts as of 13 March 2026, when none of them had expired yet:
curl -X GET "https://api.massive.com/v3/reference/options/contracts?underlying_ticker=SPY&expiration_date=2026-03-20&as_of=2026-03-13&limit=1000&apiKey=YOUR_API_KEY"
Response, trimmed to the first contract:
{
"results": [
{
"expiration_date": "2026-03-20",
"strike_price": 245,
"ticker": "O:SPY260320C00245000"
}
],
"status": "OK"
}
The same 528 contracts in the same order, with no next_url.
With as_of before the expiration and expired=true. The endpoint asks for contracts that had already expired on 13 March 2026:
curl -X GET "https://api.massive.com/v3/reference/options/contracts?underlying_ticker=SPY&expiration_date=2026-03-20&as_of=2026-03-13&expired=true&limit=1000&apiKey=YOUR_API_KEY"
Response
{
"results": [],
"status": "OK"
}
Nothing, because on 13 March 2026 these contracts were still a week from expiring.
How expired and as_of work together
The options contracts endpoint judges expired against the as_of date, and the documentation gives both defaults: as_of is today and expired is false. With expired=false the endpoint returns contracts that had not expired on the as_of date; with expired=true it returns only contracts that had. A contract still counts as not expired on its own expiration date.
These calls, all for SPY contracts expiring on 20 March 2026, show the rule:
| as_of | expired | Contracts returned |
|---|---|---|
| Not set, so today | Not set, so false | 0 |
| Not set, so today | true | 528 |
| 2026-02-02 | Not set, so false | 384 |
| 2026-03-13 | Not set, so false | 528 |
| 2026-03-13 | true | 0 |
| 2026-03-20, the expiration date | Not set, so false | 528 |
| 2026-03-21, the day after | Not set, so false | 0 |
| 2026-03-23 | true | 528 |
Two consequences follow for the options contracts endpoint:
- To see the chain as it stood on a past day, set as_of to that day and leave expired out.
- A range of expirations that spans the as_of date needs two calls, one with expired=true for the expirations already past and one without it for the rest. Neither value of expired returns both.
How to pull a whole options chain efficiently
To pull a whole options chain from the options contracts endpoint, let the endpoint do the filtering and page through the result:
- Filter by underlying_ticker and expiration_date, or by a range with expiration_date.gte and expiration_date.lte. Add contract_type, strike_price.gte or strike_price.lte to narrow it further.
- Set limit=1000, the maximum. The default is 10, and a larger value is refused with HTTP 400 rather than capped.
- Request each response's next_url with &apiKey=YOUR_API_KEY added, until a response comes back without a next_url.
For example, every SPY contract that expired in March 2026:
curl -X GET "https://api.massive.com/v3/reference/options/contracts?underlying_ticker=SPY&expiration_date.gte=2026-03-01&expiration_date.lte=2026-03-31&expired=true&limit=1000&apiKey=YOUR_API_KEY"
Response, trimmed to the first contract:
{
"results": [
{
"expiration_date": "2026-03-02",
"strike_price": 500,
"ticker": "O:SPY260302C00500000"
}
],
"status": "OK",
"next_url": "https://api.massive.com/v3/reference/options/contracts?cursor=..."
}
Following next_url to the end returns 8,356 contracts across 22 expiration dates in 9 responses, every one of them a SPY contract expiring in March 2026. The next_url keeps your filters but not your API key, which is why you add the key to it.
What the contract records do not include
The options contracts endpoint returns contract definitions: the ticker, the underlying, call or put, exercise style, expiration date, strike price, shares per contract and primary exchange. Its records carry no price, volume, open interest, greeks or quotes, and the response attributes in the All Contracts documentation list none. Use the contract tickers it returns with these endpoints instead:
- Open interest, greeks, implied volatility and the latest daily bar come from the option chain snapshot, /v3/snapshot/options/{underlyingAsset}, which returns up to 250 contracts per page and is included in Options Starter and above. The snapshot takes no date, so it shows current values only.
- Past prices come from the custom bars endpoint, /v2/aggs/ticker/{optionsTicker}/range/..., which is included in every Options plan and answers for expired contracts too.
For example, the daily bar for one of the expired SPY contracts above, a week before it expired:
curl -X GET "https://api.massive.com/v2/aggs/ticker/O:SPY260320C00650000/range/1/day/2026-03-13/2026-03-13?apiKey=YOUR_API_KEY"
Response, trimmed:
{
"ticker": "O:SPY260320C00650000",
"results": [
{ "v": 1148, "o": 22.9, "c": 17.22, "h": 24.83, "l": 16.8, "t": 1773374400000 }
],
"status": "OK"
}
t is 1773374400000 milliseconds, the start of 13 March 2026 in Eastern Time, and the contract closed at 17.22 on 1,148 contracts traded.
Why this happens
The options contracts endpoint returns nothing for a past expiration because it lists contracts as of one date, and that date is today unless you set as_of. A contract whose expiration has passed is expired as of today, the default expired=false leaves it out, and the expiration_date filter then matches nothing. An empty list with status OK is the endpoint's answer for a query that matches no contracts, so it looks the same as a date on which nothing expired.
If you see an error
"status": "OK" with an empty results array from the options contracts endpoint is not an error message, so check these in order:
- The contracts had expired by the as_of date, which is today unless you set it. Fix: add expired=true, or set as_of to the expiration date and leave expired out. Both return every contract for that expiration; an earlier as_of leaves out contracts listed after that day.
- The underlying is an index written with its prefix, such as I:SPX. The options contracts endpoint needs the bare root symbol. Fix: use underlying_ticker=SPX. See Does Massive support options data for index contracts?
- Nothing expired on that date. SPY contracts with expiration_date=2026-03-22, a Sunday, return no results even with expired=true.
{"status":"ERROR","error":"Failed to parse query parameters from URL: Key: 'ListOptionsContractsQueryParam.Limit' Error:Field validation for 'Limit' failed on the 'max' tag"} with HTTP 400 means limit is above 1000. Fix: set limit=1000 and page with next_url.
{"status":"ERROR","error":"API Key was not provided"} with HTTP 401 on a next_url request means the key was not added to it. Fix: append &apiKey=YOUR_API_KEY to the next_url.

