# Option Chain API

> POST /api/option-chain reference: full U.S. option chains with strikes, expirations, bid/ask, OI, IV, and Greeks.

> **Beta.** This API is in beta testing. The request and response schema (fields,
> units, and semantics) may change.

> **Start here**
>
> Follow the [Option Chain quickstart](/docs/option-chain-quickstart), review the [request filters](#request), or jump to the [response fields](#response-core-fields-always-returned).

The Option Chain API exposes per-contract chain rows for an underlying, for a given trading date:

```text
POST https://www.optiondata.io/api/option-chain
```

It is included with the realtime plan — both `trialing` and `active` realtime subscriptions get full access.

## Authentication

Send `Authorization: Bearer YOUR_API_KEY` for API clients. The header takes precedence over the legacy body `api_key`; malformed authorization is rejected. If neither is supplied, the portal can use the signed-in session. Live access requires an active or trialing Pro subscription.

## Request

JSON and form (`application/x-www-form-urlencoded` / `multipart/form-data`) bodies are supported. Field names are **snake_case**.

Required:
- `symbol`: one underlying ticker (uppercased by the server; 1–16 chars).

Optional:
- `api_key`: `cus_...` (raw Stripe customer id) or a generated `apikey_...` token. If omitted, the signed-in session is used.
- `date`: `YYYY-MM-DD`. Defaults to the latest available trading session. Check `meta.trading_date` for the date returned; an explicit date is never substituted with another session. An impossible calendar date (for example `2026-02-30`) returns `400`; a date with no stored session (weekend, holiday, future, or outside retention) returns `404`.
- `expiration_date`: `YYYY-MM-DD`; must be a real calendar date.
- `put_call`: `CALL` or `PUT`.
- `strike`: exact positive strike price; cannot be combined with `strike_min` or `strike_max`.
- `strike_min`, `strike_max`: inclusive strike bounds; either bound may be used alone.

Strike bounds must be positive, and `strike_min` must not exceed `strike_max`.

## Source and semantics

Responses include the complete available chain inventory matching your filters, including contracts with no trades. During U.S. market hours, newer trade-derived values are normally incorporated about every **90 seconds**: quotes, Greeks, IV, volume, open interest, and last price. Contracts without a newer trade retain their chain values until a newer chain snapshot is available. This cadence is an expected update interval, not a guarantee that every contract's quotes or Greeks refresh on each interval. Prices are in USD; Greeks/IV are decimals (e.g. IV `0.22` = 22%).

Before a new session's chain is available, requests without `date` can return the previous session. Always check `meta.trading_date` instead of assuming that the latest available session is today's date.

If intraday updates are temporarily unavailable, a response can retain earlier chain values. Check both `meta.trading_date` and `meta.as_of` before treating a response as current. An explicit historical `date` selects that session's stored chain; it does not replay the chain at a particular time within the session.

## Response — core fields (always returned)

`{ "data": [ ... ], "meta": { "trading_date": "YYYY-MM-DD", "as_of": "..." } }`

| Field | Type | Notes |
|---|---|---|
| `option_symbol` | string | OCC option symbol |
| `put_call` | `CALL` \| `PUT` | |
| `strike` | number | Strike price |
| `expiration_date` | string | `YYYY-MM-DD` |
| `bid` | number \| null | Best bid recorded with the contract's most recent trade in the session; `null` when the contract has not traded in the session |
| `ask` | number \| null | Best ask recorded with the contract's most recent trade in the session; `null` when the contract has not traded in the session |
| `last_price` | number \| null | Latest trade price |
| `open_interest` | integer string \| null | Open interest as a JSON decimal string |
| `open_interest_change` | integer string \| null | `open_interest − prior-day OI`; null when prior-day OI is unavailable |
| `volume` | integer string \| null | Daily volume as a JSON decimal string |
| `implied_volatility` | number \| null | Decimal; `null` when no model value is available |
| `delta` / `gamma` / `theta` / `vega` | number \| null | Greeks (decimal); `null` together with `implied_volatility` when no model value is available |

Numeric fields are `null` when the value is unavailable for a contract. `bid` and `ask` come from the quote attached to the contract's latest trade in the `meta.trading_date` session, so a contract that last traded early in the day carries that earlier quote. `bid` and `ask` are null together, and `implied_volatility` and the Greeks are null together. A single `0`, such as a bid of `0` with a positive ask, is a real value. This endpoint returns chain fields only; use the realtime WebSocket for flow fields such as premium, size, and trade count.

`open_interest`, `open_interest_change`, and `volume` are serialized as decimal strings so that 64-bit values stay exact. Parse them with an integer type that is safe in your language; do not assume every 64-bit integer can be represented exactly by a JavaScript `number`.

## Response — `meta`

| Field | Notes |
|---|---|
| `trading_date` | Trading session returned, in `YYYY-MM-DD` format |
| `as_of` | Newest contributing chain snapshot time or merged trade time, as a UTC ISO timestamp or `null`. This is a response-level freshness signal, not the request time or a per-contract quote/Greek timestamp |

## Errors

All errors return `{ "error": { "code": "...", "message": "..." } }` with an HTTP status. A chain that exists but has no contract matching your filters is not an error: it returns `200` with an empty `data` array.

| Status | Meaning |
|---|---|
| `400` | Invalid body or failed request validation, including an impossible calendar date |
| `401` | Could not resolve a customer (missing/invalid `api_key` and no session) |
| `403` | Resolved, but no active/trialing realtime subscription |
| `404` | `SNAPSHOT_NOT_FOUND`: no chain is stored for the requested date. `SYMBOL_NOT_FOUND`: no chain is stored for the symbol on that session |
| `405` | `METHOD_NOT_ALLOWED`: use `POST`; the `Allow` header lists the supported method |
| `422` | Request was too broad for the response guardrail |
| `429` | Rate limited; honor `Retry-After` |
| `504` | Query timed out |
| `500` | Unexpected server error |
| `503` | `UPSTREAM_UNAVAILABLE`: data or API key verification is temporarily unavailable; retry after the `Retry-After` seconds, with backoff |

A `403` (`SUBSCRIPTION_REQUIRED`) also carries `reason` and `billing_url` inside `error`:

```json
{
  "error": {
    "code": "SUBSCRIPTION_REQUIRED",
    "message": "An active or trialing subscription is required.",
    "reason": "NO_ACTIVE_SUBSCRIPTION",
    "billing_url": "/billing"
  }
}
```

`reason` is always `NO_ACTIVE_SUBSCRIPTION`: the customer was resolved, but has no active or trialing Pro subscription. A Pro subscription whose renewal payment failed more than 7 days into the unpaid billing period gets the same response until the invoice is paid. `billing_url` is the path on `https://www.optiondata.io` where the account owner can restore Pro access. Do not retry until access is restored.

## FAQ

**Q: How often should I poll?**  
A: The latest session updates about every **90 seconds** during market hours, so poll every **one to two minutes** at most and compare `meta.as_of` before doing new work. A contract without a recent trade keeps its last chain value, so an update does not refresh every Greek. Faster polling returns the same data and may hit rate limits.

**Q: Rate and size limits?**  
A: Keep sustained traffic at **one request per second per customer** and short bursts to **10 requests or fewer**, for example when fetching several expirations. Current throttling is best-effort; this is client usage guidance, not a guaranteed global quota. HTTP **429** includes a `Retry-After` header; wait that long before retrying. See [API rate limits](/docs/api-rate-limits) for the complete policy.

**Planned, not yet active:** trial **20/minute and 600/hour**; paid Pro **60/minute and 3,000/hour**. Once enabled, both limits apply per customer across their keys and IPs; reaching either triggers rate limiting. No effective date has been announced. Enterprise limits are contract-specific. See [API rate limits](/docs/api-rate-limits) for the complete policy.

Response-size behavior is unchanged: over-broad chains are rejected instead of returning partial results—use expiration, put/call, or strike filters.

**Q: History range and delay?**  
A: Chain sessions from about **2026-02-20** forward (grows each trading day). Use `as_of` for freshness. For individual trades, use Historical SQL: paid access covers the past **15** days (≥**15**-minute delay).

**Q: Free trial / extension?**  
A: Complete qualification, then explicitly activate the **14-day** Pro trial when you are ready. Eligible trial users can receive **50% off for life with a promotion code from Sales**; contact Sales for the code, then enter it on Billing before the trial ends. Trials are not auto-extended.

**Q: WebSocket full chain?**  
A: No. Chains are REST only; WebSocket is trades/flow.
