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
REST requests authenticate with the X-API-Key header. WebSocket connections use the ?api_key= query parameter instead — browsers cannot set headers on a WebSocket handshake, so the two transports differ here.
# REST — header auth
curl -H "X-API-Key: ow_pro_abc123..." https://api.optionwhales.io/v1/flow/current
# WebSocket — query-parameter auth (a WS handshake cannot carry headers)
wscat -c "wss://api.optionwhales.io/v1/ws/abnormal-trades?api_key=ow_pro_abc123..."Note: REST endpoints accept the header only. Passing ?api_key= to a REST route returns 401 missing_api_key.
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-trades 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 Conventions
Field names alone are not enough for financial data. These rules hold across every endpoint, so you can write a parser once instead of inferring conventions from examples.
null is not zero
null means not observed or not computed. 0 means a measured zero. They are never used interchangeably, and a nullable numeric is never disguised as 0. Treat every price, Greek and IV field as nullable — enrichment is incomplete on a small share of rows (about 5% of live tape rows carry a null implied_volatility), and one null will poison a downstream average if you coerce it.
Units, scales and signs
- Implied volatility is decimal, not percent —
0.1238is 12.38%. - Ranks and percentiles are 0–1, not 0–100.
- Premium and notional are US dollars, already multiplied by contract size.
ncounts observations in a window, not calendar days. Endpoints reportingnalso report thefrom/todates of that sample.- Signed premium follows the aggressor: BUY (at ask) is
+premium paid, SELL (at bid) is-premium received. Puts are not sign-flipped anywhere in the API.
How a direction was determined — three different things
Every direction-bearing field declares its basis, because these are not interchangeable and only one of them is reported by an exchange:
aggressor_nbbo— inferred from where the print landed against the NBBO. Exchanges do not publish aggressor side, so this is our classification;direction_confidence(0–1) says how strongly.nbbo_location_proxy(dark pool) — a proxy. It does not identify an institution and does not prove buyer or seller intent.- Multi-leg membership is genuinely exchange-confirmed (OPRA condition codes 232–247), but the structure name (vertical, calendar, butterfly…) is our geometric classification of the legs, not an exchange label.
What is calibrated, and what is not
Most scores on this API are descriptive statistics, not validated predictors. Where an endpoint has never been tested against subsequent price movement it says so in its own payload — Tide, for example, carries basis.calibration: "none". Do not read a confidence field as a calibrated probability unless the endpoint states that it is.
The one directional construction we have validated out-of-sample is published: Option Flow Predicts Returns — Call Imbalance in OTM institutional blocks, OOS Sharpe 1.6–1.8 over a 416-session holdout. In the same study, composite directional signals and buy-side-only pressure were eliminated entirely, and put imbalance reversed. Treat that as the evidence bar.
Sessions and timestamps
session is the US market session a payload describes (YYYY-MM-DD, US Eastern trading day). Endpoints that compute rather than merely read also return as_of (the newest observation actually included — real data freshness) and computed_at (when the response was assembled). On a quiet tape those differ noticeably, and staleness should be judged from as_of. Where present, finality distinguishes a provisional session still accumulating from a settled final one.
A "current" endpoint returns the latest session that has data — which after the close, at a weekend, or on a holiday is the last completed session, not today. Confluence on the returned session value rather than assuming two endpoints describe the same day.
Quota model
Four independent budgets, not one. A request can be refused by any of them:
| Budget | Pro | Where reported |
|---|---|---|
| Requests / minute | 60 | X-RateLimit-* on every response |
| Requests / day | 5,000 | not yet in a header — see Known gaps |
| Flow rows / day | 150,000 | X-FlowRows-* on tape endpoints |
| Watchlist size / distinct adds per day | 25 / 50 | GET /v1/quotes/watchlist |
The per-minute window is fixed, not rolling: the counter is keyed to the wall-clock minute, and X-RateLimit-Reset is the unix second at which it rolls. Honour Retry-After when present — it takes precedence.
Every request costs quota, including the ones that fail
The counter increments before the limit is tested, so a request that comes back 429 has already spent a slot on both the minute and day counters. Retrying into a 429 is the fastest way to lose a day of budget. Back off to X-RateLimit-Reset instead.
Score methodology — net_delta_last and directional_score
Both are weighted constructions, not raw sums, so neither can be reproduced from a field name alone.
/v1/flow/current — rankings[].net_delta_last
Signed dollar-delta of the most recent 5-minute cluster only (not cumulative for the session). Per order:
delta_$ = option_delta
x spot
x contracts (order-level total_contracts)
x multiplier (shares_per_contract, default 100)
x dir_score (+conf buy / -conf sell / +conf x 0.25 neutral)
x close_mult (0.40 likely-closing / 0.70 uncertain / 1.00 default)
x dte_mult (0.60 if DTE<=1 / 0.85 if DTE<=5 / 1.00 otherwise)
net_delta_last = sum(delta_$) over the latest 5-minute clusterSo yes — option delta, total contracts and the 100 multiplier are all applied. But three further weights are applied that the field name does not imply: dir_score scales by our direction confidence and carries the sign, close_mult discounts trades inferred to be closing, and dte_mult discounts short-dated ones. Unit is USD of delta exposure, signed (positive = net long delta bought). It is a confidence-weighted, discounted dollar delta — not reproducible from delta x contracts x 100. The same six factors build net_gamma_last (spot squared), net_vega_last and net_theta_last (no spot term).
/v1/directional/current — the four pressures and directional_score
Each pressure is the summed notional (USD) of that session's large orders in that bucket, and the values are returned already signed by directional meaning:
call_buy_pressure positive (bullish)
call_sell_pressure negative (bearish)
put_buy_pressure negative (bearish)
put_sell_pressure positive (bullish)
directional_score = call_buy + call_sell + put_buy + put_sell
= (CB + PS) - (CS + PB) in unsigned terms
imbalance_score = (|CB| - |PB|) / (|CB| + |PB|) in [-1, +1]So yes, directional_score is the plain sum of the four fields as returned — because the sign is already baked into each one. Note this is the put-flipped directional construction (buying a put is bearish, selling one is bullish), which is what distinguishes it from Tide's net_premium, an accounting measure that does not flip puts.
imbalance_score uses absolute call and put buy pressure, so it measures call-vs-put concentration, not direction. Both are raw notional — no volatility adjustment, no cross-sectional normalisation — so magnitudes are not comparable across tickers; a mega-cap will dominate a small-cap on size alone.
A missing ticker does not mean "no evidence"
/v1/directional/current truncates by row order, so absence is not a statement about the ticker. Two caps apply: your max_display (10–500), and a server-side ceiling of 500 rows. Compare tickers_count against the length of data to detect truncation — but note tickers_count is itself capped at 500, and the echoed max_display reflects the cache build parameter rather than your request.
There is no batch parameter for an explicit ticker list on either endpoint. For a known set, use /v1/flow/{session}/{ticker} or /v1/directional/{ticker}/history per name, or pull the whole session once via /v1/directional/{session} and filter client-side — one request rather than N.
Changelog
Dated, newest first. Methodology changes are listed separately from additive field changes so you can tell which require re-reading numbers.
| Date | Kind | Change |
|---|---|---|
| 2026-08-24 | docs | Published the equations for net_delta_last, the four directional pressures, directional_score and imbalance_score; documented display-cap truncation. |
| 2026-08-23 | fix | /v1/volatility/{ticker}/iv-rank returned n with from/to null. Window is now derived from the observations used; added unit and rank_scale. |
| 2026-08-23 | methodology | Tide methodology_version 1.1.0 — DTE bucketing corrected; a prior build placed every order in 0dte. Net-premium totals unaffected; by_dte and breakdown changed. |
| 2026-08-23 | added | Tide gains as_of, computed_at, finality, and basis.measure_type/calibration/methodology_version. |
| 2026-08-16 | changed | Flow-tape per-request limit raised from 500 to 2,000. Over-limit values are clamped, not rejected. |
Known gaps
Rather than leave you to find these during an integration, the current limitations of this reference:
- No machine-readable OpenAPI document yet. Response schemas are documented per endpoint here but are not downloadable or CI-validated.
- The daily request budget is enforced but not yet surfaced as a response header — only the per-minute budget is.
GET /v1/quotes/watchlistreports the distinct-additions limit but not how many you have already used today./v1/screenerreturnssession: "latest"rather than the resolved date other endpoints return.- Multi-leg legs expose
expirationasMM/DDwith no year and carry nodte; parse the OCC symbol for an unambiguous date. - Derived-score methodology (intent, directional) is not yet published at the level needed to reproduce a value independently.
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"
# the 0+1 bucket — both spellings work
"...?dte_filter=0+1"
"...?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.
Every GEX response names its gamma basis — read it
All GEX numbers are computed on a declared gamma_basis, returned on summary (and at the top level on /timeseries). The current basis is parity_display_spot: one gamma per (strike, expiry) pair, evaluated at the spot the response reports. Values computed before 2026-08-28 carry vendor_snapshot — the option vendor's own greeks, which were struck at the snapshot's capture-time underlying rather than at our display spot. The two are not comparable; on NVDA 2026-08-26 they differ by 1.84×.
GET /v1/gex/{ticker}/timeseries can return a window that straddles that change. It does not drop points — that would silently shorten your chart — so it tells you instead: basis_mixed: true, gamma_bases lists what is present, and gamma_basis is null. Each point also carries its own gamma_basis. If you plot a mixed window unsegmented you will see a step change at the boundary that is a basis change, not a market event.
Start with /with-flow — it is the endpoint our own dashboard renders
Two GEX endpoints exist and they do not return the same numbers. /v1/gex/{session}/{ticker} is structural only on a past session — open interest, no flow. /with-flow adds option flow on every session: the live streaming tape for the current one, and that session's archived large-order tape for a past one. It is the same computation behind the GEX & Strike panel on optionwhales.io.
This matters for levels, not just totals. Call wall, put wall and the gamma flip are derived from structural + flow, so the two endpoints can name different strikes. On SPY 2026-09-01 the call wall is 800 without flow and 780 with it, and net GEX is −$7.31B vs −$9.08B. If you alert on a level, read it from /with-flow.
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 declaring the measure type and calibration status. Pro keys only.
Read this before assigning a direction to Tide
net_premium is an accounting measure, not a directional one. It is premium paid minus premium received, and puts are not sign-flipped — buying a put and buying a call both count as +premium. The payload states this in basis.measure_type (accounting_net_premium).
A directional reading is computable from the four component fields, which every response exposes:
directional = (call_premium_bought + put_premium_sold)
- (call_premium_sold + put_premium_bought)The two genuinely diverge. On SPY for the 2026-08-21 session the accounting figure was -48,238,882 while the directional one was -37,488,418 — and they can disagree in sign, not only magnitude.
Tide is not calibrated against forward returns. It is a descriptive statistic, and basis.calibration says so in every payload. Tide assigns no bullish/bearish label, and integrations should present it as raw context rather than a sentiment verdict.
If you want a directional signal, start elsewhere. Our published study Option Flow Predicts Returns — a Pan & Poteshman replication over 188.5M intraday orders across 1,250 sessions (2021–2026), screened through a three-stage IS1/IS2/OOS framework — found that Call Imbalance in OTM institutional blocks held up out-of-sample (Sharpe 1.6–1.8), while composite directional signals and buy-side-only pressure were eliminated entirely. Put imbalance reversed (−3.98 Sharpe in mid-cap), consistent with put flow being hedging rather than informed direction — so treating put selling as bullish is the assumption the data argues hardest against.
Freshness and versioning
Recomputed on demand behind a 120-second TTL. Each response carries as_of (timestamp of the newest order actually included — real data freshness), computed_at (when the response was assembled; on a quiet tape the two differ noticeably), finality (provisional while a session is still accumulating, final once settled), and basis.methodology_version. Version 1.1.0 corrected DTE bucketing: a prior build parsed the raw MM/DD expiry field and silently placed every order in 0dte. Net-premium totals were unaffected; only by_dte and breakdown were wrong.
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.