Apexon BLOG

FINMARKETS

Fetching a full options chain with one REST call

Options data is the classic "annoying to source" dataset — multiple expirations, two sides per strike, prices moving all session — so here's the one-call version: pull the full chain, read the headers that tell you how fresh it is, and skip the guesswork.

Apexon team··7 min read

An options chain is the complete set of calls and puts listed for a single underlying, grouped by expiration date, with strike, bid/ask, volume, and open interest attached to every contract. It's one of those datasets that's simple to describe and annoying to actually source yourself: multiple expirations, two sides per strike, prices moving throughout the trading day. FinMarkets returns it with a single REST call — one request gets you the nearest expiration's full chain plus the list of every other expiration date available, and you decide from there whether you need more.

The alternative most people reach for first is stitching it together by hand: one call to find out what expirations even exist, then a separate call per expiration to fetch calls and puts, then merging the results into something usable before you can run any analysis on it. That's not hard, exactly, it's just plumbing you shouldn't have to write more than once. The rest of this post covers what the response actually contains, how to get more than one expiration without writing that plumbing yourself, and how to tell — from the response, not from a guess — whether what you got back is fresh.

What's in an options chain response?

A call to GET /v1/stock/{symbol}/options returns the upstream-shaped payload as-is, nested under optionChain.result[0]. Three things live there: a quote object for the underlying itself, an expirationDates array listing every expiration date on file, and an options array holding the calls and puts for whichever expiration the call resolved to — the nearest one, by default. A hasMiniOptions flag sits alongside them:

{
  "optionChain": {
    "result": [{
      "quote": { … },
      "expirationDates": [ … ],
      "hasMiniOptions": false,
      "options": [{
        "calls": [ … ],
        "puts": [ … ]
      }]
    }]
  }
}

What's guaranteed by the route itself is that top-level shape: quote, expirationDates, hasMiniOptions, and options[].calls / options[].puts. The fields inside each individual contract are whatever the upstream source publishes — this API doesn't rename or strip anything, so the definitive per-contract field list is the response itself, or the schema in the interactive explorer, not a hand-maintained table on this page.

Called with no query parameters, the route resolves to the nearest upcoming expiration — that's the one expiration whose calls and puts arrays are actually populated in the response. Every other date on file still shows up in expirationDates, as an epoch-seconds timestamp, but you won't have contract data for those dates until you ask for one specifically. That's the split worth remembering: one call always tells you what exists, and gets you one expiration's contracts for free.

One call, whole chain

Ask for a symbol and you get the nearest expiration's full chain in the same response as every other expiration date that exists:

curl -s "https://api.apexon.dev/v1/stock/AAPL/options" \
  -H "X-API-Key: fm_live_XXXX"

If that's the only expiration you need, you're done. To pull additional expirations, read the dates out of expirationDates and pass each one back in as date (a unix timestamp), one call per expiration:

import requests

BASE = "https://api.apexon.dev/v1/stock/AAPL/options"
HEADERS = {"X-API-Key": "fm_live_XXXX"}

first = requests.get(BASE, headers=HEADERS).json()
result = first["optionChain"]["result"][0]
expirations = result["expirationDates"]

for exp in expirations[1:4]:
    leg = requests.get(BASE, headers=HEADERS, params={"date": exp}).json()
    opts = leg["optionChain"]["result"][0]["options"][0]
    print(exp, len(opts["calls"]), "calls", len(opts["puts"]), "puts")

There's also GET /v1/stock/{symbol}/options/bulk, which does that fan-out on the server side: pass max_expirations (default 6) or an explicit comma-separated dates list and it fetches every requested expiration upstream for you, returning them all — expirationDate, calls, puts per chain — in one response instead of one request per expiration on your end. However many expirations you ask for, the endpoint won't fan out past twelve upstream calls in a single request; ask for more than that and it caps at twelve rather than erroring.

How fresh is it? Read the headers

Options prices move throughout the session, so freshness matters more here than on something like quarterly financials. Rather than assume a cache window, read it off the response: X-Cache tells you whether what came back was served from cache (HIT) or fetched from upstream just now (MISS); X-Response-Time-ms is how long the server took to build that specific response; X-RateLimit-Limit, -Remaining, and -Reset tell you where you stand against your per-minute budget before you fire the next poll; and X-Plan confirms which plan the key resolved to, in case you're debugging why a limit looks different than expected. There's no Age header on these responses — X-Cache and X-Response-Time-ms are what you have, and they're enough to build a polling loop that backs off instead of guessing.

A quick way to see all of this at once is to ask curl for the response headers along with the body:

curl -si "https://api.apexon.dev/v1/stock/AAPL/options" \
  -H "X-API-Key: fm_live_XXXX" | grep -i "^x-"

If X-Cache comes back HIT on two calls in a row, you're reading a cached copy either way — polling faster won't get you newer data, it'll just spend rate-limit budget for nothing. For the fuller picture on how caching and rate limits interact across the API, not just this endpoint, see the honest guide to API rate limits and caching.

Screening for candidates first

If you don't already know which symbols you want chains for, the screener endpoints get you there before you touch the options route at all. GET /v1/screener/predefined/{scr_id} runs a canned screen — most_actives and day_gainers are two of the ids in the catalog — and returns a page of matching symbols:

curl -s "https://api.apexon.dev/v1/screener/predefined/most_actives?count=10" \
  -H "X-API-Key: fm_live_XXXX"

POST /v1/screener takes a JSON body — query, size, offset, sort_field, sort_type, quote_type — for a custom filter instead of a predefined one, like every equity above a market-cap floor in a given region:

curl -s -X POST "https://api.apexon.dev/v1/screener" \
  -H "X-API-Key: fm_live_XXXX" \
  -H "Content-Type: application/json" \
  -d '{
    "size": 25,
    "sort_field": "intradaymarketcap",
    "sort_type": "DESC",
    "quote_type": "EQUITY",
    "query": {"operator": "and", "operands": [
      {"operator": "eq", "operands": ["region", "us"]},
      {"operator": "gte", "operands": ["intradaymarketcap", 10000000000]}
    ]}
  }'

Either endpoint hands back symbols, not chains — pull each symbol's chain with the options endpoint above once you know which ones you actually care about. That two-step split matters for a practical reason too: fetching a chain is a heavier request than fetching a quote, so it's worth narrowing your symbol list with a screener before you fan out into options calls for names you were never going to trade anyway.

FAQ

Can I get historical options chains?

No — the options endpoint returns the current chain, and the date parameter selects which future expiration cycle you're looking at, not a past point in time. There's no endpoint for retrieving what a chain looked like on a previous date; everything you pull is current as of the moment you call it.

How often should I poll?

There's no single right interval — it depends on your rate limit and how time-sensitive your use case is. Check X-RateLimit-Remaining before firing the next request and back off as it approaches zero, and use X-Cache to see whether you're actually reaching upstream (MISS) or getting a cached copy (HIT) so you're not polling faster than the data underneath is changing.

Is Greeks data included?

The options endpoint passes upstream's per-contract fields through unchanged — FinMarkets doesn't add, compute, or strip anything on top of what the source returns. Whether a specific field like delta or implied volatility is present isn't something this post enumerates; check a live response, or the schema in the interactive explorer at api.apexon.dev/docs, before you build logic that depends on one being there.

What auth do I need?

Send your key in the X-API-Key header — X-API-Key: fm_live_<key>. Keys are scoped per product, so an fm_ key authenticates against FinMarkets only, not CatalogAIO or LineSnap.

See the full options and screener reference

Every query parameter and predefined screen id lives in the FinMarkets docs, alongside the interactive explorer for checking exactly what a live response contains.

Explore the API reference More posts