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

1

Get your API key

Go to your Account page and generate a new API key. Keep it safe — it's shown only once.

2

Make your first request

Include your key in the X-API-Key header.

bash
curl -H "X-API-Key: YOUR_API_KEY" \
  https://api.optionwhales.io/v1/flow/current
3

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

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

TierPer MinutePer DayWebSocket
Free10200Not available
Pro605,0002 per endpoint

Rate limit headers are included in every response:

http
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 58
X-RateLimit-Reset: 1700000060

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

http
X-FlowRows-Limit: 150000
X-FlowRows-Remaining: 148750
X-FlowRows-Reset: 1700006400

Exhausting 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:

SelectionRowsRequestsBudget-days
One mid-liquidity name (SOXL)35,624180.2
Four such names299,0831502.0
NVDA + TSLA1,020,5305116.8
SPY alone1,644,37782311.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:

json
"data": {
  "realtime": false,
  "delay_seconds": 900,
  "delay_reason": "policy_off_watchlist",
  "as_of": "2026-08-08T15:24:32Z"
}
FeedFreshnessWhy
Stock quotes (watchlisted)Real-time
OHLC candles, non-watchlisted ticker15-min delayed (today only)Entitlement
Flow tape, current session, on watchlist60s delayedEntitlement
Flow tape, current session, off watchlist15-min delayedEntitlement
Flow tape, past sessionsCompleteImmutable — never delayed
Dark pool / off-exchange prints~15-min delayedSource licence — no fresher data exists
Open interest, GEX, IV surfaceAs-of last snapshotSnapshot 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

FeatureFreePro
Flow rankingsTop 3 tickers, limited fieldsAll tickers, all fields
Flow sessions listBlockedFull access
Ticker detailBlockedFull access
Momentum rankingsTop 3 tickers, limited fieldsAll tickers, all fields
Momentum historyBlockedMulti-session
Dark pool (blocks, pressure, alerts, prints)BlockedFull access
Abnormal trades (top ~1% of the tape)Last 5, limited fieldsFull history, all fields
Options flow tape (contract level)Blocked90d history, 50k rows/day
Flow tape freshness (current session)15-min delayed; 60s on watchlist
WebSocket streamingNot availableReal-time stream
Earnings IntelligenceBlockedFull access
Economic CalendarBlockedFull access
ETF Flow (SPY/QQQ/IWM/TLT)BlockedFull access
Directional Score / ScatterBlockedFull access
GEX (Gamma Exposure)BlockedFull access
Real-time stock quotesBlockedWatchlist (25 symbols)
Real-time quote stream (WS)Not available1 Hz push
Intraday OHLC candlesBlockedAny 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.

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

bash
wss://api.optionwhales.io/v1/ws/abnormal-trades?api_key=YOUR_PRO_KEY

Filter by tickers (send after connecting)

json
{"type": "subscribe", "tickers": ["AAPL", "NVDA", "TSLA"]}

Python Example

python
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

javascript
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_trade

New abnormal trade detected. Contains full trade data in the data field.

heartbeat

Sent every 30 seconds to keep the connection alive. Contains ts timestamp.

subscribed

Confirmation after sending a subscribe message. Contains the active tickers filter list.

error

Error 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

bash
wss://api.optionwhales.io/v1/ws/flow?api_key=YOUR_PRO_KEY

Subscribe (required — nothing flows until you do)

json
{"type": "subscribe", "tickers": ["SPY", "QQQ", "NVDA"]}

Python Example

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

json
{
  "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 key
4003Key tier below Pro
4008Connection limit reached for this key (two per endpoint; each WebSocket endpoint counts separately)
4029Daily flow-row budget exhausted — resets at 00:00 UTC

Health Check

The health endpoint requires no authentication and returns the service status.

bash
curl https://api.optionwhales.io/health
json
{
  "status": "healthy",
  "service": "pro-api",
  "version": "1.0.0",
  "ws_connections": 0
}

Error Codes

CodeDescription
401Missing or invalid API key
403Insufficient tier (endpoint requires Pro+)
429Rate limit exceeded — check Retry-After header
502Upstream data service unavailable

Ready to build?

Generate your API key and start integrating OptionWhales data into your trading workflow.