# 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`. If omitted, the latest available date in `mv_contract_rank_flow` is used.
- `expiration_date`: `YYYY-MM-DD`.
- `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

Reads `mv_contract_rank_flow`, a per-contract-per-day materialized mart, with aggregate-state merge functions grouped by `option_symbol` for the requested `date`. Several fields are derived in the query (see the Response tables). Prices are in USD; Greeks/IV are decimals (e.g. IV `0.22` = 22%).

## Response — core fields (always returned)

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

| Field | Type | Notes |
|---|---|---|
| `symbol` | string | Underlying ticker |
| `option_symbol` | string | OCC option symbol |
| `put_call` | `CALL` \| `PUT` | |
| `strike` | number \| null | Strike price |
| `expiration_date` | string \| null | `YYYY-MM-DD` |
| `bid` | number \| null | Best bid |
| `ask` | number \| null | Best ask |
| `last_price` | number \| null | Latest trade price |
| `open_interest` | integer string \| null | JSON decimal string from ClickHouse `Int64` |
| `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 |
| `delta` / `gamma` / `theta` / `vega` | number \| null | Greeks (decimal) |

Numeric fields are `null` when the value is unavailable for a contract. 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 to preserve the existing ClickHouse `Int64` wire behavior. 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:

| Status | Meaning |
|---|---|
| `400` | Invalid body or failed request validation |
| `401` | Could not resolve a customer (missing/invalid `api_key` and no session) |
| `403` | Resolved, but no active/trialing realtime subscription |
| `422` | Request was too broad for the response guardrail |
| `429` | Rate limited; honor `Retry-After` |
| `504` | Query timed out |
| `500` | Unexpected server error |

## FAQ

**Q: How often should I poll?**  
A: Broad/full-chain: about every **5 minutes**. Narrow symbol/expiry/strike: every **1–2 minutes**. Faster polling rarely helps and may hit rate limits.

**Q: Rate and size limits?**  
A: The **current default** is about **60 requests / 60 seconds** per customer on this endpoint, for trial and paid users, enforced on a best-effort basis. HTTP **429** includes `Retry-After`.

**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 trade ticks back to **2025-02-18**, use Historical SQL (≥**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 next steps shown inside the authenticated portal. Trials are not auto-extended.

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