Pro API Documentation
Programmatic access to OptionWhales intent flow, momentum, abnormal trades, earnings intelligence, economic events, directional scores, GEX, and dark pool / off-exchange data.
Quick Start
Get your API key
Go to your Account page and generate a new API key. Keep it safe — it's shown only once.
Make your first request
Include your key in the X-API-Key header.
curl -H "X-API-Key: YOUR_API_KEY" \
https://api.optionwhales.io/v1/flow/currentExplore endpoints below
Free keys get limited data. Upgrade to Pro for full access including WebSocket streaming.
Authentication
All API requests require an API key passed via the X-API-Key header or as a ?api_key= query parameter.
# Header (recommended)
curl -H "X-API-Key: ow_pro_abc123..." https://api.optionwhales.io/v1/flow/current
# Query parameter
curl "https://api.optionwhales.io/v1/flow/current?api_key=ow_pro_abc123..."Never share your API key or commit it to source control. Use environment variables.
Rate Limits
| Tier | Per Minute | Per Day | WebSocket |
|---|---|---|---|
| Free | 10 | 200 | Not available |
| Pro | 60 | 5,000 | 2 per endpoint |
Rate limit headers are included in every response:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 58
X-RateLimit-Reset: 1700000060When rate-limited, you receive a 429 Too Many Requests with a Retry-After header.
Rejected requests still count
The counter increments before the limit is tested, so a request that comes back 429 has already consumed a slot on both the per-minute and per-day counters. Retrying straight into a 429 burns daily quota without returning any data — the fastest way to lose a day's budget. Honour Retry-After, or back off until X-RateLimit-Reset, rather than retrying immediately.
WebSockets are metered separately. /v1/ws/flow and /v1/ws/abnormal do not draw on the per-minute or per-day request counters at all — a streaming connection costs you no REST quota. They do share the daily flow-row budget below.
Row budget — flow tape only
The /v1/options-flow tape endpoints and the /v1/ws/flow stream are metered in rows as well as requests: Pro keys may draw 150,000 rows per UTC day, counted per account rather than per key and shared between REST and the WebSocket. Rows are charged after filtering, so you are never billed for rows a filter removed, and the two aggregate endpoints (/summary and /contracts) do not draw on it at all.
X-FlowRows-Limit: 150000
X-FlowRows-Remaining: 148750
X-FlowRows-Reset: 1700006400Exhausting it returns 429 with { "error": "flow_row_budget_exhausted" } on REST, or closes the WebSocket with code 4029. The budget resets at 00:00 UTC.
Sizing it against real flow. The budget is set so a full trading day of streaming on a handful of liquid names fits comfortably. For scale, a recent session recorded ~29,000 orders on SPY, ~14,000 on QQQ and ~8,500 on NVDA, with ten of the most active tickers together totalling ~63,000 — so 5–10 tickers streamed all session sits well inside 150,000, while index-heavy selections are the ones to watch. Track X-FlowRows-Remaining and narrow with min_premium or min_contracts if you approach it.
Page size vs. budget. Pro keys may request up to 2,000 rows per request on /tape and /contract/{occ}; a larger limit is clamped rather than rejected, so paging code does not need to know the cap. Note that page size changes how many round trips a backfill takes, not how much data you may draw — 2,000-row pages spend the same 150,000 daily rows in 75 requests that 500-row pages spend in 300.
Backfilling history
There is no bulk download or CSV export today, and none is scheduled. It is a substantial piece of infrastructure rather than a setting we can enable, so we would rather say so plainly than imply it is close. Everything currently goes through the paginated JSON endpoints.
You do not need a request per contract, though. Pass ticker + session to /tape and page with older_than — that is one request per 2,000 rows, not per contract. The daily request cap is almost never what binds you; rows are. Measured against a real session, a full 90-day backfill costs:
| Selection | Rows | Requests | Budget-days |
|---|---|---|---|
| One mid-liquidity name (SOXL) | 35,624 | 18 | 0.2 |
| Four such names | 299,083 | 150 | 2.0 |
| NVDA + TSLA | 1,020,530 | 511 | 6.8 |
| SPY alone | 1,644,377 | 823 | 11.0 |
Requests are at limit=2000, against a 5,000/day cap — so even the heaviest row here uses 16% of your requests. Index names are where a backfill gets slow, and they are exactly where min_premium or min_contracts pays for itself, since rows removed by a filter are never charged.
Data Freshness
Not every feed is real-time, and the reasons differ. Endpoints that carry a delay say so in their own response, so you never have to infer it:
"data": {
"realtime": false,
"delay_seconds": 900,
"delay_reason": "policy_off_watchlist",
"as_of": "2026-08-08T15:24:32Z"
}| Feed | Freshness | Why |
|---|---|---|
| Stock quotes (watchlisted) | Real-time | — |
| OHLC candles, non-watchlisted ticker | 15-min delayed (today only) | Entitlement |
| Flow tape, current session, on watchlist | 60s delayed | Entitlement |
| Flow tape, current session, off watchlist | 15-min delayed | Entitlement |
| Flow tape, past sessions | Complete | Immutable — never delayed |
| Dark pool / off-exchange prints | ~15-min delayed | Source licence — no fresher data exists |
| Open interest, GEX, IV surface | As-of last snapshot | Snapshot cadence (up to ~12h) |
delay_reason distinguishes a delay we cannot shorten (source_license) from one tied to your entitlement (policy_off_watchlist), which adding the ticker to your real-time watchlist reduces.
Free vs Pro
| Feature | Free | Pro |
|---|---|---|
| Flow rankings | Top 3 tickers, limited fields | All tickers, all fields |
| Flow sessions list | Blocked | Full access |
| Ticker detail | Blocked | Full access |
| Momentum rankings | Top 3 tickers, limited fields | All tickers, all fields |
| Momentum history | Blocked | Multi-session |
| Dark pool (blocks, pressure, alerts, prints) | Blocked | Full access |
| Abnormal trades (top ~1% of the tape) | Last 5, limited fields | Full history, all fields |
| Options flow tape (contract level) | Blocked | 90d history, 50k rows/day |
| Flow tape freshness (current session) | — | 15-min delayed; 60s on watchlist |
| WebSocket streaming | Not available | Real-time stream |
| Earnings Intelligence | Blocked | Full access |
| Economic Calendar | Blocked | Full access |
| ETF Flow (SPY/QQQ/IWM/TLT) | Blocked | Full access |
| Directional Score / Scatter | Blocked | Full access |
| GEX (Gamma Exposure) | Blocked | Full access |
| Real-time stock quotes | Blocked | Watchlist (25 symbols) |
| Real-time quote stream (WS) | Not available | 1 Hz push |
| Intraday OHLC candles | Blocked | Any ticker (live: watchlist) |
Endpoints
Intent Flow
Momentum
Abnormal Trades
Account
Earnings Intelligence
Option order signals, BS Greeks, and directional intent for tickers with earnings events. Pro keys only — no free-tier preview.
Economic Events & ETF Flow
Macro event calendar and ETF option flow for SPY, QQQ, IWM, TLT. Pro keys only.
Directional Score
Composite bull/bear score derived from net notional, order clustering, and IV signals — the data behind the Market Intelligence scatter plot. Pro keys only.
GEX — Gamma Exposure
Per-strike gamma exposure profiles, key price levels (gamma flip, max pain, call/put wall), and multi-session GEX trends. Pro keys only.
Filtering by expiry — dte_filter
Every GEX endpoint takes the same five buckets: all, 0dte, 0+1, weekly, monthly. They are inclusive upper bounds counted in TRADING days, not calendar days — so on a Friday, 0+1 covers Friday and Monday, and 0dte is same-day expiry only.
# 0DTE gamma profile
curl -H "X-API-Key: $KEY" "https://api.optionwhales.io/v1/gex/2026-08-14/SPY/greek-exposure?dte_filter=0dte"
# 0DTE intraday GEX, sampled every 30 minutes
curl -H "X-API-Key: $KEY" "https://api.optionwhales.io/v1/gex/2026-08-14/SPY/intraday-exposures?dte_filter=0dte&step_min=30"
# note: 0+1 must be URL-encoded, a bare + decodes to a space
"...?dte_filter=0%2B1"A plain integer of days is still accepted for backwards compatibility and is mapped to the nearest bucket (0→0dte, 1→0+1, ≤7→weekly, ≤30→monthly, above that→all). Prefer the bucket names — they say what you mean.
Where the key levels live, and how gamma_flip is computed
gamma_flip, max_pain and the walls are returned by two endpoints: nested under summary.key_levels on the full profile, and flat at the top level on /levels. They are not on /greek-exposure, which is a second-order greeks decomposition computed from a different pipeline. Both honour dte_filter, so the levels you get back match the bucket you asked for.
gamma_flip is not a per-strike sign change, so do not try to derive it from by_strike. It is derived from where the cumulative net-GEX curve crosses zero, interpolated between the two bracketing strikes and resolved to the crossing nearest spot — so it lands between strikes and need not sit where any single strike flips sign. On 2026-08-17 SPY 0DTE it was 777.45: the cumulative curve crossed zero between strikes 777 and 778, even though strike 777's own net GEX was +892M. Both are correct — they measure different things.
The full profile labels which detection stage produced the number. gamma_flip_type: "global_cumulative" is the cumulative crossing above. On heavily one-sided chains the cumulative sum never crosses zero, and it falls back to "local_per_strike" — a per-strike sign change with noise filters. Both raw values are always returned separately as global_gamma_flip and local_gamma_transition, alongside gamma_regime (long/short). Worth checking the type before you act on the level: the same ticker can switch stage between dte_filter buckets.
Real-Time Stock Quotes & Candles
Live underlying prices and intraday OHLC candles, sourced from a direct market-data feed (sub-second spot). Pro keys only — free tier receives no real-time quotes. Real-time symbols are bounded by a per-key watchlist: the product is option-driven, so you stream the handful of underlyings you trade, not the whole market.
Watchlist caps (Pro): up to 25 active real-time symbols, and at most 50 distinct symbols added per UTC day. Quotes for non-watchlisted symbols are rejected; candles for a non-watchlisted ticker's current session are served 15 minutes delayed (delayed: true). Historical sessions are always full.
Implied Volatility
Implied-volatility analytics: the full IV surface, ATM-IV term structure, multi-session ATM-IV(30d) history, per-cell IV percentile / z-score / bands, batch ATM-IV, and top IV movers. Pro keys only.
Open Interest
Open-interest built from twice-daily (AM/PM ET) full-chain snapshots: per-ticker OI and volume timeseries, snapshot-to-snapshot OI change, latest snapshot, and per-contract OI lookups by OCC symbol. Pro keys only.
Net-Premium Tide
Signed net-premium flow — market-wide, per-sector, and per-ticker — split call/put, bucketed by DTE and moneyness, with an intraday cumulative curve. Built from large orders only (not the full tape); direction is an aggressor-NBBO classification (BUY = premium paid, SELL = premium received). Every response carries a basis block. Pro keys only.
Option Contracts
Per-contract addressability by OCC symbol: chain enumeration, ATM chains and expiry breakdown for an underlying, plus a contract profile, daily OHLC history and intraday minute bars. Chain / OI / IV / greeks are as-of the last snapshot (up to ~12h stale); daily bars reach ~1yr back, intraday is a recent window. Pro keys only.
Options Flow Tape — contract level
The recorded large-order tape at OCC-contract granularity. Every option order whose grouped size cleared 100 contracts, across the full listed universe — order-grouped, direction-classified and IV/delta-enriched. This is our own recording, not a raw vendor print feed.
How this differs from the two neighbouring endpoints. /v1/abnormal-trades returns the top ~1% of this same recording (orders above their ticker's p99 size threshold). /v1/contracts/{occ}/trades returns the raw exchange print tape. This family is the full recorded order flow.
Freshness. Historical sessions are immutable and always served in full. For the current session, rows are delayed 15 minutes unless every ticker in the request is on your real-time watchlist, in which case the delay is 60 seconds. Every response states its own delay in data.delay_seconds.
Row budget. Tape endpoints and the /v1/ws/flow stream share a daily allowance of 150,000 rows per account (not per key), reported in X-FlowRows-Remaining and reset at 00:00 UTC. The two aggregate endpoints — /summary and /contracts — are exempt and never delayed. History reaches back 90 days; Pro keys only.
Two things to handle when you build a table off these rows. direction is our classification, not exchange-reported — exchanges do not publish aggressor side, so we infer it, and direction_confidence (0–1) tells you how strongly. Filter on min_confidence if you only want high-conviction rows. Separately, implied_volatility and delta are nullable — enrichment can be incomplete on a small share of rows (enrich_status says which). Treat them as optional rather than assuming a float, or a single null will poison a downstream average.
Empty responses explain themselves. A response with no rows carries a reason when one exists: stale_live_window: true plus a hint means the trailing live window is empty because the market is closed (the hint names the session to request instead), and degraded: true means the live feed itself failed upstream — retry in a few seconds rather than treating the empty page as final. Both markers appear on every endpoint in this family; a healthy response carries neither.
MCP Server — AI Agents
A Model Context Protocol (MCP) endpoint that exposes the API as agent tools for Claude Desktop, Cursor, and other MCP clients — JSON-RPC 2.0 over Streamable HTTP. Authenticate with your Pro key in the X-API-Key header. Tools include intent flow, directional score, abnormal trades, GEX levels, dark-pool ranking, earnings, economic calendar, and IV analytics.
Seasonality
Average close-to-close return by calendar month, with the historical hit rate, computed over a multi-year window. Pro keys only.
Fundamentals & Corporate Actions
Company financial statements plus dividend, split, and IPO history. Pro keys only.
SEC Filings — Insider & 13F
SEC Form 4 insider transactions (ticker-centric) and 13F-HR institutional holdings (by filer CIK). Pro keys only.
Technical Indicators
SMA / EMA / RSI / MACD time series on adjusted closes. Pro keys only.
News & Sentiment
Recent ticker news, each article tagged with a pre-computed per-ticker sentiment (positive / negative / neutral) and a short reasoning. Pro keys only.
Short Selling
FINRA short-interest (bi-weekly settlement) and short-volume (daily, with a per-venue NYSE / Nasdaq / ADF split). Pro keys only.
Congress Trades
Congressional stock trades from House (Clerk PTR filings) and Senate disclosures. PTR transactions (not holdings) — bracketed amounts and a ~45-day disclosure lag; narrative/context, not a real-time signal. Pro keys only.
FDA / Catalyst Calendar
Upcoming pharma/biotech catalysts — Phase 2/3 clinical-trial readout dates (primary-completion) from ClinicalTrials.gov, joined to tickers via the SEC company spine. These are estimated readout dates (they slip), not PDUFA decision dates — a narrative/context calendar to cross-reference against options flow, not a real-time signal. Pro keys only.
Multi-Signal Screener
Rank and filter tickers across our signals in one call — options-flow net premium, day-over-day OI change, upcoming FDA catalysts, and recent congressional activity. The differentiator is the cross-signal filter ("bullish flow AND rising OI AND an FDA catalyst within 30 days") that no single endpoint does. Each result lists which signals fired. Pro keys only.
Dark Pool / Off-Exchange
Off-exchange (TRF-reported) equity prints and the analytics built on them: detected block trades with rolling-percentile baselines and %ADV, NBBO-location flow pressure (per window and cumulative intraday curves), options-confirmation alerts, whole-market rankings, and the raw print tape. ~90 days of history, ~400-ticker calibrated universe. Pro keys only.
Data basis (also returned in every response's data block): ~15-minute delayed, off-exchange TRF prints from Nasdaq-reported venues only — not consolidated tape and not real-time. Off-exchange trades carry no buy/sell side; all directionality is an NBBO-location proxy (at-ask … at-bid), never a definitive buy or sell.
WebSocket Streaming
Pro keys only
Connect to the WebSocket endpoint for real-time abnormal trade detection. Trades are pushed to your connection as they are detected during market hours.
Connection URL
wss://api.optionwhales.io/v1/ws/abnormal-trades?api_key=YOUR_PRO_KEYFilter by tickers (send after connecting)
{"type": "subscribe", "tickers": ["AAPL", "NVDA", "TSLA"]}Python Example
import asyncio
import json
import websockets
API_KEY = "ow_pro_your_key_here"
URL = f"wss://api.optionwhales.io/v1/ws/abnormal-trades?api_key={API_KEY}"
async def stream_trades():
async with websockets.connect(URL) as ws:
print("Connected! Waiting for trades...")
async for message in ws:
data = json.loads(message)
if data.get("type") == "abnormal_trade":
trade = data["data"]
print(f"{trade['ticker']} {trade['side']} ${trade['premium']:,.0f}")
elif data.get("type") == "heartbeat":
print(".", end="", flush=True)
asyncio.run(stream_trades())JavaScript Example
const API_KEY = "ow_pro_your_key_here";
const url = `wss://api.optionwhales.io/v1/ws/abnormal-trades?api_key=${API_KEY}`;
const ws = new WebSocket(url);
ws.onopen = () => console.log("Connected!");
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === "abnormal_trade") {
console.log(`${data.data.ticker} ${data.data.side} $${data.data.premium}`);
}
};
ws.onerror = (err) => console.error("WebSocket error:", err);
ws.onclose = () => console.log("Disconnected");Message Types
abnormal_tradeNew abnormal trade detected. Contains full trade data in the data field.
heartbeatSent every 30 seconds to keep the connection alive. Contains ts timestamp.
subscribedConfirmation after sending a subscribe message. Contains the active tickers filter list.
errorError message. The connection may be closed after this.
Flow Tape WebSocket
Pro keys only — watchlist-bounded
The push counterpart of /v1/options-flow/tape: every recorded order (100+ contracts, order-grouped, direction-classified, IV/delta-enriched) for your subscribed tickers is pushed the moment its enrichment completes — typically 1–3 seconds behind the print. Subscribe once at the open and accumulate the day's tape without polling.
Subscriptions are bounded by your real-time watchlist (the same one used by /v1/quotes — manage it via PUT /v1/quotes/watchlist). Symbols outside the watchlist are named in rejected rather than silently dropped. Two concurrent connections per key on this endpoint. Streamed rows draw on the same daily flow-row budget as the REST tape; when it runs out the socket closes with code 4029.
Connection URL
wss://api.optionwhales.io/v1/ws/flow?api_key=YOUR_PRO_KEYSubscribe (required — nothing flows until you do)
{"type": "subscribe", "tickers": ["SPY", "QQQ", "NVDA"]}Python Example
import asyncio
import json
import websockets
API_KEY = "ow_pro_your_key_here"
URL = f"wss://api.optionwhales.io/v1/ws/flow?api_key={API_KEY}"
async def stream_flow():
async with websockets.connect(URL) as ws:
await ws.send(json.dumps({"type": "subscribe",
"tickers": ["SPY", "QQQ", "NVDA"]}))
async for message in ws:
msg = json.loads(message)
if msg.get("type") == "flow_order":
o = msg["data"]["order"]
print(f"{msg['data']['ticker']} {o['option_ticker']} "
f"x{o['total_contracts']} {o['direction']} "
f"${o['premium']:,.0f}")
elif msg.get("type") == "subscribed":
print("streaming:", msg["tickers"], "rejected:", msg["rejected"])
asyncio.run(stream_flow())Order Event
Each flow_order carries the recorded order under data.order in its captured form — field names match the recording (e.g. total_contracts, option_type), not the REST tape's normalised row shape. data.event_id is stable per order and matches the REST tape, so the two sources can be joined.
{
"type": "flow_order",
"data": {
"ticker": "SPY", "event_id": "327394957d78e1e5976e2d6b7c8fc74c",
"session_date": "2026-08-10", "ts_ms": 1786392892183,
"order": {
"timestamp": "2026-08-10 20:14:52.183000+00:00",
"option_ticker": "O:SPY260810C00721000", "option_type": "Call",
"strike": 721.0, "expiration": "2026-08-10",
"total_contracts": 301, "premium": 37625.0, "total_notional": 21716330.0,
"avg_price": 1.25, "avg_bid": 1.24, "avg_ask": 1.26,
"direction": "buy", "direction_confidence": 0.94, "order_type": "sweep",
"spot_price": 721.4, "implied_volatility": 0.196, "delta": 0.329,
"trade_count": 9, "exchange_count": 3, "time_span_ms": 412,
"enrich_status": "complete"
}
}
}Close Codes
4001Missing or invalid API key4003Key tier below Pro4008Connection limit reached for this key (two per endpoint; each WebSocket endpoint counts separately)4029Daily flow-row budget exhausted — resets at 00:00 UTCHealth Check
The health endpoint requires no authentication and returns the service status.
curl https://api.optionwhales.io/health{
"status": "healthy",
"service": "pro-api",
"version": "1.0.0",
"ws_connections": 0
}Error Codes
| Code | Description |
|---|---|
401 | Missing or invalid API key |
403 | Insufficient tier (endpoint requires Pro+) |
429 | Rate limit exceeded — check Retry-After header |
502 | Upstream data service unavailable |
Ready to build?
Generate your API key and start integrating OptionWhales data into your trading workflow.