Bullflow API Docs
JSON data endpoints and Server-Sent Events (SSE) streams for Bullflow alerts, replay tools, and option analytics. Supply a valid Bullflow API key via the key query parameter where required.
Base URL: https://api.bullflow.io
Transports: HTTPS JSON and Server-Sent Events (Content-Type text/event-stream)
POST /mcp
Connect coding agents directly to Bullflow through the MCP server. This endpoint exposes Bullflow public API docs, net GEX, net VEX, and net premium lookups, last trade price lookups, peak return lookups, custom alert creation, streaming endpoint metadata, and bounded backtesting replay samples over Streamable HTTP.
Connection
- Endpoint:
https://api.bullflow.io/mcp - Transport:
MCP Streamable HTTP - Supported method:
POST - Unsupported methods return an MCP error response.
GETandDELETEare not valid for this endpoint. - When using SSE endpoint metadata returned by MCP tools, it is recommended to implement auto reconnect functionality in your client in case the stream is disconnected for any reason.
Authentication
- Query string:
?key=YOUR_API_KEY - Header:
X-API-Key: YOUR_API_KEY - Header:
Authorization: Bearer YOUR_API_KEY - Invalid or missing keys are returned as JSON-RPC errors from the MCP endpoint.
Available tools
bullflow_api_docs— returns API docs and MCP connection details.bullflow_peak_return— runs the peak return lookup with the current MCP connection's API key.bullflow_net_gex— runs the net GEX lookup with the current MCP connection's API key.bullflow_net_vex— runs the net VEX lookup with the current MCP connection's API key.bullflow_net_premium_series— returns cumulative call and put net premium values with the current MCP connection's API key.bullflow_last_trade_price— runs the last trade price lookup with the current MCP connection's API key.bullflow_dark_pool_trades— runs the dark pool trades lookup with the current MCP connection's API key.bullflow_get_custom_alerts— returns saved custom alerts for the current MCP connection's API key account.bullflow_create_alert— creates a saved custom alert for the current MCP connection's API key account.bullflow_streaming_endpoint— returns connection details for live alerts or backtesting replay SSE.bullflow_backtesting_replay_sample— collects a bounded replay sample for agent workflows.
Resource
bullflow://api-docs— YAML docs resource exposed by the MCP server.
Example agent config
{
"mcpServers": {
"bullflow": {
"type": "streamable-http",
"url": "https://api.bullflow.io/mcp?key=YOUR_API_KEY"
}
}
}If your MCP client supports headers instead of query-string auth, use X-API-Key or Authorization: Bearer YOUR_API_KEY.
GET /v1/streaming/alerts
Streams Bullflow algo alerts in real time plus any custom alerts that match the account associated with your API key. Only alerts created after the connection opens are delivered, so reconnect if you miss time on the stream.
Query params
key— required API key value generated in the dashboard.
Event payloads
- Each SSE
data:frame contains JSON. Parse that JSON and useeventto identify the payload type. event === "init"— providesmode(alerts) andstartedAt(ISO timestamp).event === "heartbeat"— emitted every 10 seconds to keep the connection alive; safe to ignore.event === "alert"— includes the alertidand the alert payload indata.- Alert payloads include
data.alertType,data.symbol,data.alertName,data.alertPremium,data.averageFillPrice, anddata.timestamp. data.alertType— eitheralgo(Bullflow algo alerts such as Urgent Repeater, Sizable Sweep, etc.) orcustom(alerts matching the custom alerts you set in the Bullflow dashboard).- Alert payloads also include
data.latency,data.deliveryLatency, anddata.receivedAt. - Authentication, subscription, and connection-throttle failures are returned as normal HTTP JSON errors before the SSE stream begins.
- Custom alert delivery is limited to 10 custom alerts per 60 seconds and 1000 custom alerts per 12 hours. API subscribers receive up to 60 custom alerts per 60 seconds and 5000 custom alerts per 12 hours.
- It is recommended to implement auto reconnect functionality in your SSE client in case the stream is disconnected for any reason. Live alert streams only deliver alerts created after the connection opens.
Example payload
{
"event": "alert",
"id": "1MPDp-Qm6_urgent",
"data": {
"alertType": "algo",
"symbol": "O:AMD251205P00205000",
"alertName": "Urgent Repeater",
"alertPremium": 16965.0,
"averageFillPrice": 1.31,
"timestamp": 1764708086.0,
"latency": 0.842,
"deliveryLatency": 0.091,
"receivedAt": 1764708086.751
}
}alertPremium is the premium of the trade that triggered the alert; averageFillPrice is the average fill price for that trade.
Python example code
import json
import requests
from types import SimpleNamespace
with requests.get(
"https://api.bullflow.io/v1/streaming/alerts",
params={"key": "YOUR_API_KEY"},
stream=True,
) as stream:
stream.raise_for_status()
for line in stream.iter_lines(decode_unicode=True):
if not line or not line.startswith("data: "):
continue
message = json.loads(line[6:], object_hook=lambda d: SimpleNamespace(**d))
event = getattr(message, "event", None)
if event == "init":
print("connected at", getattr(message, "startedAt", "unknown"))
if event == "heartbeat":
continue
if event == "alert":
alert = getattr(message, "data", None)
if alert:
print(
"⚡ alert",
getattr(message, "id", ""),
getattr(alert, "symbol", ""),
"premium",
getattr(alert, "alertPremium", ""),
"avg fill",
getattr(alert, "averageFillPrice", ""),
"type",
getattr(alert, "alertType", ""),
"name",
getattr(alert, "alertName", ""),
)
GET /v1/streaming/backtesting
Replays one historical trading day over SSE using your saved custom alerts plus Bullflow algo alerts from the regular session. The stream loads the day's trades, evaluates every trade against your current custom alert set, then emits matched custom alerts and algo alerts in replay order.
Query params
key— required API key value.date— required replay date inYYYY-MM-DDformat. Must be on or after2025-06-01and cannot be in the future.speed— optional replay speed multiplier. Passing1replays in real time.
Event payloads
- Each SSE
data:frame contains JSON. Parse that JSON and useeventto identify the payload type. event === "init"— includesmode(alerts-backtesting),replayDate, andconnected.event === "status"— a single initialization message:Initializing trades, custom alerts and algo alerts... This can take up to a minute.event === "ready"— includesreplayDate,speed,totalTrades,totalCustomAlerts,processedTrades,customAlerts,algoAlerts, andtotalReplayAlerts.event === "heartbeat"— emitted during replay gaps and includesplaybackTimesuch as10:42:14 EST.event === "alert"— includessequence,id, and adataobject withalertType,symbol,alertName,alertPremium,tradePrice,timestamp, andestTimestamp.data.alertType— eitheralgoorcustom.event === "complete"— includesalertsSent,totalTrades,customAlerts,algoAlerts,totalReplayAlerts, andelapsedSeconds.event === "error"— sent before the stream closes if the request cannot be loaded or replayed.- It is recommended to implement auto reconnect functionality in your SSE client in case the stream is disconnected for any reason.
Example payload
{
"event": "alert",
"id": "16",
"sequence": 16,
"data": {
"alertType": "custom",
"symbol": "O:SPY260409P00676000",
"alertName": "Ultra Repeater",
"alertPremium": 63022,
"tradePrice": 1.31,
"timestamp": 1775672380,
"estTimestamp": "2026-04-09 10:46:20 EST"
}
}Backtesting replays both matched custom alerts and algo alerts in timestamp order.
Python example code
import json
import requests
with requests.get(
"https://api.bullflow.io/v1/streaming/backtesting",
params={"key": "YOUR_API_KEY", "date": "2026-04-08", "speed": 60},
stream=True,
) as stream:
stream.raise_for_status()
for line in stream.iter_lines(decode_unicode=True):
if not line or not line.startswith("data: "):
continue
message = json.loads(line[6:])
event = message.get("event")
if event == "status":
print("status:", message.get("message"))
continue
if event == "heartbeat":
print("heartbeat:", message.get("playbackTime"))
continue
if event == "alert":
alert = message.get("data") or {}
print(
"alert",
message.get("sequence"),
alert.get("alertType"),
alert.get("symbol"),
alert.get("alertName"),
alert.get("tradePrice"),
alert.get("estTimestamp"),
)
continue
if event == "complete":
print("finished replay:", message.get("alertsSent"), "events sent")
breakPOST /v1/alerts/create-alert
Creates a saved custom alert for the account associated with your API key.
Authentication
- Query string:
?key=YOUR_API_KEY - Header:
X-API-Key: YOUR_API_KEY - Header:
Authorization: Bearer YOUR_API_KEY
JSON body
name— required human-readable alert name.- Numeric range fields use clean min/max names:
premiumMin/premiumMax,dteMin/dteMax,otmPercentMin/otmPercentMax,scoreMin/scoreMax,strikeMin/strikeMax,impliedVolatilityMin/impliedVolatilityMax, and the other range fields shown in the YAML download. - Ticker filters:
tickerBlocklistexcludes tickers,tickerAllowlistlimits matches to specific tickers, and values are uppercased and deduped. - Quick filters: pass user-facing chip labels in
quickFilters. - Sector filter:
sectorCodedefaults to12. - Include switches:
includeCalls,includePuts,includeAskSide,includeBidSide,includeMid,includeBullish,includeBearish,includeNeutral,includeSingles,includeSweeps,includeSplits,includeBlocks,includeMultiLeg, andincludeExDividend.
Quick filter labels
Send labels in quickFilters. The tooltip text below describes what each quick filter means.
| Label | Tooltip |
|---|---|
| ETFs | ETFs |
| Stocks | Stocks |
| Sweeps | Trades executed rapidly across multiple exchanges |
| Calls | Call options |
| Puts | Put options |
| Bid | Trades executed on the bid side |
| Ask | Trades executed on the ask side |
| AA | Above Ask. They really wanted these contracts! |
| Unusual | Trade size exceeding open interest |
| Urgent | Rapid repeat trades matching Bullflow criteria |
| Bullflow | Aggressive repeat trades matching Bullflow criteria. |
| Position Builders | Consistent repeat trades matching Bullflow criteria |
| Sizable | Large Unusual Trades |
| Grenade | Risky short term trades |
| 100k+ | Trades with $100,000+ value |
| Whales | $1M+ |
| Rising Vol | First trade that hits the scanner where volume exceeds OI |
| AM Spike | Volume exceeding 15% of the OI, 25k OI minimum, before 11am EST |
| LEAPS | Long dated expirations |
| High Sig | SigScore 0.8+ |
| Repeat Flow | Contracts that have 3 or more size>OI trades. |
| Large Size | Size > 5000 |
| Earnings Soon | Earnings within 2 days |
| Vol>OI | Volume greater than open interest |
| Weeklies | Trades expiring within a week |
| Penny Stocks | Stock price < $5 |
| Oddities | Stocks not seen on Bullflow in a day |
| Unicorns | Stocks not seen on Bullflow in a week or longer |
| OTM | Out of the money |
| Bullish Flow | Stocks with 80%+ bullish/bearish ratio |
| > Avg | Total premium has exceeded the daily average for this stock. |
Validation
- Unknown fields are rejected. Numeric fields must be finite, and each min value must be less than or equal to its matching max value.
- Missing optional fields use broad defaults.
Example request
{
"name": "Large bullish SPY flow",
"premiumMin": 50000,
"tickerAllowlist": ["SPY"],
"includePuts": false,
"includeBearish": false,
"quickFilters": ["100k+"]
}Example response
{
"id": "abc123",
"status": "created",
"alertName": "Large bullish SPY flow"
}JavaScript example code
const response = await fetch(
"https://api.bullflow.io/v1/alerts/create-alert?key=YOUR_API_KEY",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: "Large bullish SPY flow",
premiumMin: 50000,
tickerAllowlist: ["SPY"],
includePuts: false,
includeBearish: false,
quickFilters: ["100k+"]
})
}
);
if (!response.ok) {
throw new Error(await response.text());
}
console.log(await response.json());GET /v1/alerts/custom-alerts
Returns the saved custom alerts for the account associated with your API key.
Authentication
- Query string:
?key=YOUR_API_KEY - Header:
X-API-Key: YOUR_API_KEY - Header:
Authorization: Bearer YOUR_API_KEY
Response
count— number of saved custom alerts returned.alerts— saved alerts, including each alertid.quickFilters— user-facing quick filter labels for the alert.
Example response
{
"count": 1,
"alerts": [
{
"id": "abc123",
"alertName": "Large bullish SPY flow",
"minPremium": 50000,
"maxPremium": 5000000000,
"quickFilters": ["100k+"],
"timestamp": "2026-05-13T18:24:00.000Z"
}
]
}JavaScript example code
const response = await fetch(
"https://api.bullflow.io/v1/alerts/custom-alerts?key=YOUR_API_KEY"
);
if (!response.ok) {
throw new Error(await response.text());
}
console.log(await response.json());GET /v1/data/peakReturn
Returns the highest percent return reached by an option contract after the trade timestamp you provide.
Query params
key— required API key value.sym— required option symbol, for exampleO:SPY260408C00520000.old_price— required entry price for the contract.trade_timestamp— required Unix timestamp for the trade.
Behavior
- Successful responses return a JSON object with the peak price and the peak percent return since the provided timestamp.
- Missing or invalid API keys return normal HTTP JSON errors before any lookup runs.
- This endpoint is rate-limited to 60 requests per minute per API key.
Example response
{
"peakPriceSinceTimestamp": "125.47",
"peakPercentReturnSinceTimestamp": 12.22
}Python example code
import requests
response = requests.get(
"https://api.bullflow.io/v1/data/peakReturn",
params={
"key": "YOUR_API_KEY",
"sym": "O:SPY260408C00520000",
"old_price": 1.35,
"trade_timestamp": 1775669400,
},
timeout=30,
)
response.raise_for_status()
print(response.json())GET /v1/data/netgex
Returns net GEX by strike and expiration and removes the aggregate gamma field from every strike row.
Query params
key— required API key value.ticker— required underlying ticker symbol, for exampleSPY.symis also accepted as an alias.
Behavior
- Successful responses return a JSON object with
ticker,spot_price,as_of, andstrikes. - Each strike row includes
expiration,strike,call_gex,put_gex,net_gex,call_gamma, andput_gamma. - The response intentionally omits
gamma. - This endpoint is rate-limited to 25 requests every minute per API key.
Example response
{
"ticker": "SPY",
"spot_price": 719.245,
"as_of": "2026-04-30T19:24:26.236Z",
"strikes": [
{
"expiration": "20260430",
"strike": 659,
"call_gex": 63681.28,
"put_gex": 0,
"net_gex": 63681.28,
"call_gamma": 0.00123,
"put_gamma": 0
}
]
}Python example code
import requests
response = requests.get(
"https://api.bullflow.io/v1/data/netgex",
params={
"key": "YOUR_API_KEY",
"ticker": "SPY",
},
timeout=30,
)
response.raise_for_status()
print(response.json())GET /v1/data/netvex
Returns net VEX by strike and expiration and removes the aggregate vanna field from every strike row.
Query params
key— required API key value.ticker— required underlying ticker symbol, for exampleSPY.symis also accepted as an alias.
Behavior
- Successful responses return a JSON object with
ticker,spot_price,as_of, andstrikes. - Each strike row includes
expiration,strike,call_vex,put_vex,net_vex,call_vanna, andput_vanna. - The response intentionally omits
vanna. - Net VEX uses
spot_price * vanna * open_interestand calculatesnet_vexas call VEX plus put VEX. - This endpoint is rate-limited to 25 requests every minute per API key.
Example response
{
"ticker": "SPY",
"spot_price": 719.245,
"as_of": "2026-04-30T19:24:26.236Z",
"strikes": [
{
"expiration": "20260430",
"strike": 659,
"call_vex": 1204.33,
"put_vex": -455.12,
"net_vex": 749.21,
"call_vanna": 1.6744,
"put_vanna": -0.6328
}
]
}Python example code
import requests
response = requests.get(
"https://api.bullflow.io/v1/data/netvex",
params={
"key": "YOUR_API_KEY",
"ticker": "SPY",
},
timeout=30,
)
response.raise_for_status()
print(response.json())GET /v1/data/netPremiumSeries
Returns chronological cumulative call and put net premium values for a ticker and inclusive date range.
Query params
key— required API key value.ticker— required underlying ticker symbol, for exampleSPY.from— required inclusive start date inYYYY-MM-DDformat.to— required inclusive end date inYYYY-MM-DDformat.period— optional aggregation period:1D,7D,1M,3M,1Y, orMAX. Defaults to1D.include_multileg— optional boolean. Defaults tofalse.sweeps_only— optional boolean. Defaults tofalse.
Behavior
- Each point contains a Unix
timestamp, cumulativecallsNetPremium, and cumulativeputsNetPremium. bucket_secondsreports the time bucket size. A value of0means individual time points.- Results are returned in chronological order.
- This endpoint is rate-limited to 30 requests every minute per API key.
Example response
{
"ticker": "SPY",
"period": "1D",
"from": "2026-07-24",
"to": "2026-07-24",
"include_multileg": false,
"sweeps_only": false,
"multilegs_filtered_count": 593,
"sweeps_filtered_count": 0,
"bucket_seconds": 0,
"point_count": 2,
"points": [
{
"timestamp": 1784899805,
"callsNetPremium": -274992,
"putsNetPremium": 0
},
{
"timestamp": 1784899812,
"callsNetPremium": -449529,
"putsNetPremium": 125000
}
]
}Python example code
import requests
response = requests.get(
"https://api.bullflow.io/v1/data/netPremiumSeries",
params={
"key": "YOUR_API_KEY",
"ticker": "SPY",
"from": "2026-07-24",
"to": "2026-07-24",
"period": "1D",
"include_multileg": False,
"sweeps_only": False,
},
timeout=30,
)
response.raise_for_status()
print(response.json())GET /v1/data/lastTradePrice
Returns the latest last trade price for a stock ticker as reported by Nasdaq.
Query params
key— required API key value.ticker— required stock ticker symbol, for exampleSPY.symis also accepted as an alias.
Behavior
- Successful responses return a JSON object with the Nasdaq-reported
lastTradePriceandtimestamp. - Missing or invalid API keys return normal HTTP JSON errors before any lookup runs.
- This endpoint is rate-limited to 60 requests every minute per API key.
Example response
{
"lastTradePrice": 719.24,
"timestamp": "2026-04-30T19:24:26.236Z"
}Python example code
import requests
response = requests.get(
"https://api.bullflow.io/v1/data/lastTradePrice",
params={
"key": "YOUR_API_KEY",
"ticker": "SPY",
},
timeout=30,
)
response.raise_for_status()
print(response.json())GET /v1/data/darkPoolTrades
Returns dark pool trades for an inclusive date range, optionally filtered by ticker.
Query params
key— required API key value.from— required start date inYYYY-MM-DDformat.to— required end date inYYYY-MM-DDformat.ticker— optional stock ticker symbol, for exampleSPY.limit— optional page size. Defaults to500and cannot exceed5000.cursor— optional pagination cursor from a previous response'snextCursorfield.minNotional— optional minimum trade notional value.
Behavior
- Date ranges are inclusive. Passing the same date for
fromandtoreturns trades for that day. - This endpoint returns trades reported through Nasdaq trade reporting data. Based on Nasdaq's public figures, FINRA/Nasdaq TRFs support more than 40% of total U.S. equities trading while off-exchange trading is over 45% of volume, making Nasdaq-reported off-exchange flow roughly 90% of U.S. off-exchange equity trading.
- The date range cannot exceed 30 days.
- Successful responses return
from,to,ticker,minNotional,count,limit,hasMore,nextCursor, androws. - When
hasMoreistrue, passnextCursorascursoron the next request to fetch the following page. - Condition codes can be cross-referenced against Nasdaq Trader's sale condition modifier definitions.
- This endpoint is rate-limited to 10 requests every minute per API key.
Example response
{
"from": "2026-04-01",
"to": "2026-04-01",
"ticker": "SPY",
"minNotional": 1000000,
"count": 1,
"limit": 500,
"hasMore": false,
"nextCursor": null,
"rows": [
{
"symbol": "SPY",
"tradeId": "123456789",
"sequence": 987654,
"exchange": "D",
"trfId": "N",
"price": 519.24,
"size": 10000,
"notional": 5192400,
"volume": 74231812,
"conditions": [37],
"sipTimestampMs": 1775067000000,
"trfTimestampMs": 1775067000123,
"pctDayVolume": 0.1348,
"volumePercent": 13.48,
"percent30DayVolume": 4.91,
"correctionOnlyUpdate": false,
"tradeDate": "2026-04-01"
}
]
}Python example code
import requests
response = requests.get(
"https://api.bullflow.io/v1/data/darkPoolTrades",
params={
"key": "YOUR_API_KEY",
"from": "2026-04-01",
"to": "2026-04-01",
"ticker": "SPY",
"limit": 500,
"minNotional": 1000000,
},
timeout=30,
)
response.raise_for_status()
print(response.json())