# 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.

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 complete 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 strike price.
- `strike_min`, `strike_max`: inclusive strike bounds; either bound may be used alone.

## Source and semantics

Responses include every listed contract that matches your filters, including contracts with no trades. During U.S. market hours the current session updates about every **90 seconds**: contracts that trade receive newer quotes, Greeks, IV, volume, open interest, and last price. Contracts without a newer trade keep their chain values. `meta.as_of` is the newest update represented by the returned rows; it does not mean every contract was refreshed at that time. Prices are in USD; Greeks/IV are decimals (e.g. IV `0.22` = 22%).

A new session becomes the default only after its complete chain is available. Until then, requests without `date` return the previous session and report its date in `meta.trading_date`.

## 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` | Most recent data update represented in the response, as a UTC ISO timestamp or `null` |

## 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` | Data is temporarily unavailable; retry with backoff |

## 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 the first year 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.
