Docs

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)

MCP

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. GET and DELETE are 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.

Alerts

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

  • keyrequired API key value generated in the dashboard.

Event payloads

  • Each SSE data: frame contains JSON. Parse that JSON and use event to identify the payload type.
  • event === "init" — provides mode (alerts) and startedAt (ISO timestamp).
  • event === "heartbeat" — emitted every 10 seconds to keep the connection alive; safe to ignore.
  • event === "alert" — includes the alert id and the alert payload in data.
  • Alert payloads include data.alertType, data.symbol, data.alertName, data.alertPremium, data.averageFillPrice, and data.timestamp.
  • data.alertType — either algo (Bullflow algo alerts such as Urgent Repeater, Sizable Sweep, etc.) or custom (alerts matching the custom alerts you set in the Bullflow dashboard).
  • Alert payloads also include data.latency, data.deliveryLatency, and data.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", ""),
                )
Backtesting

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

  • keyrequired API key value.
  • daterequired replay date in YYYY-MM-DD format. Must be on or after 2025-06-01 and cannot be in the future.
  • speed — optional replay speed multiplier. Passing 1 replays in real time.

Event payloads

  • Each SSE data: frame contains JSON. Parse that JSON and use event to identify the payload type.
  • event === "init" — includes mode (alerts-backtesting), replayDate, and connected.
  • event === "status" — a single initialization message: Initializing trades, custom alerts and algo alerts... This can take up to a minute.
  • event === "ready" — includes replayDate, speed, totalTrades, totalCustomAlerts, processedTrades, customAlerts, algoAlerts, and totalReplayAlerts.
  • event === "heartbeat" — emitted during replay gaps and includes playbackTime such as 10:42:14 EST.
  • event === "alert" — includes sequence, id, and a data object with alertType, symbol, alertName, alertPremium, tradePrice, timestamp, and estTimestamp.
  • data.alertType — either algo or custom.
  • event === "complete" — includes alertsSent, totalTrades, customAlerts, algoAlerts, totalReplayAlerts, and elapsedSeconds.
  • 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")
            break
Alerts

POST /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

  • namerequired 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: tickerBlocklist excludes tickers, tickerAllowlist limits matches to specific tickers, and values are uppercased and deduped.
  • Quick filters: pass user-facing chip labels in quickFilters.
  • Sector filter: sectorCode defaults to 12.
  • Include switches: includeCalls, includePuts, includeAskSide, includeBidSide, includeMid, includeBullish, includeBearish, includeNeutral, includeSingles, includeSweeps, includeSplits, includeBlocks, includeMultiLeg, and includeExDividend.

Quick filter labels

Send labels in quickFilters. The tooltip text below describes what each quick filter means.

LabelTooltip
ETFsETFs
StocksStocks
SweepsTrades executed rapidly across multiple exchanges
CallsCall options
PutsPut options
BidTrades executed on the bid side
AskTrades executed on the ask side
AAAbove Ask. They really wanted these contracts!
UnusualTrade size exceeding open interest
UrgentRapid repeat trades matching Bullflow criteria
BullflowAggressive repeat trades matching Bullflow criteria.
Position BuildersConsistent repeat trades matching Bullflow criteria
SizableLarge Unusual Trades
GrenadeRisky short term trades
100k+Trades with $100,000+ value
Whales$1M+
Rising VolFirst trade that hits the scanner where volume exceeds OI
AM SpikeVolume exceeding 15% of the OI, 25k OI minimum, before 11am EST
LEAPSLong dated expirations
High SigSigScore 0.8+
Repeat FlowContracts that have 3 or more size>OI trades.
Large SizeSize > 5000
Earnings SoonEarnings within 2 days
Vol>OIVolume greater than open interest
WeekliesTrades expiring within a week
Penny StocksStock price < $5
OdditiesStocks not seen on Bullflow in a day
UnicornsStocks not seen on Bullflow in a week or longer
OTMOut of the money
Bullish FlowStocks with 80%+ bullish/bearish ratio
> AvgTotal 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());
Alerts

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 alert id.
  • 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());
Data

GET /v1/data/peakReturn

Returns the highest percent return reached by an option contract after the trade timestamp you provide.

Query params

  • keyrequired API key value.
  • symrequired option symbol, for example O:SPY260408C00520000.
  • old_pricerequired entry price for the contract.
  • trade_timestamprequired 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())
Data

GET /v1/data/netgex

Returns net GEX by strike and expiration and removes the aggregate gamma field from every strike row.

Query params

  • keyrequired API key value.
  • tickerrequired underlying ticker symbol, for example SPY. sym is also accepted as an alias.

Behavior

  • Successful responses return a JSON object with ticker, spot_price, as_of, and strikes.
  • Each strike row includes expiration, strike, call_gex, put_gex, net_gex, call_gamma, and put_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())
Data

GET /v1/data/netvex

Returns net VEX by strike and expiration and removes the aggregate vanna field from every strike row.

Query params

  • keyrequired API key value.
  • tickerrequired underlying ticker symbol, for example SPY. sym is also accepted as an alias.

Behavior

  • Successful responses return a JSON object with ticker, spot_price, as_of, and strikes.
  • Each strike row includes expiration, strike, call_vex, put_vex, net_vex, call_vanna, and put_vanna.
  • The response intentionally omits vanna.
  • Net VEX uses spot_price * vanna * open_interest and calculates net_vex as 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())
Data

GET /v1/data/netPremiumSeries

Returns chronological cumulative call and put net premium values for a ticker and inclusive date range.

Query params

  • keyrequired API key value.
  • tickerrequired underlying ticker symbol, for example SPY.
  • fromrequired inclusive start date in YYYY-MM-DD format.
  • torequired inclusive end date in YYYY-MM-DD format.
  • period — optional aggregation period: 1D, 7D, 1M, 3M, 1Y, or MAX. Defaults to 1D.
  • include_multileg — optional boolean. Defaults to false.
  • sweeps_only — optional boolean. Defaults to false.

Behavior

  • Each point contains a Unix timestamp, cumulative callsNetPremium, and cumulative putsNetPremium.
  • bucket_seconds reports the time bucket size. A value of 0 means 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())
Data

GET /v1/data/lastTradePrice

Returns the latest last trade price for a stock ticker as reported by Nasdaq.

Query params

  • keyrequired API key value.
  • tickerrequired stock ticker symbol, for example SPY. sym is also accepted as an alias.

Behavior

  • Successful responses return a JSON object with the Nasdaq-reported lastTradePrice and timestamp.
  • 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())
Data

GET /v1/data/darkPoolTrades

Returns dark pool trades for an inclusive date range, optionally filtered by ticker.

Query params

  • keyrequired API key value.
  • fromrequired start date in YYYY-MM-DD format.
  • torequired end date in YYYY-MM-DD format.
  • ticker — optional stock ticker symbol, for example SPY.
  • limit — optional page size. Defaults to 500 and cannot exceed 5000.
  • cursor — optional pagination cursor from a previous response's nextCursor field.
  • minNotional — optional minimum trade notional value.

Behavior

  • Date ranges are inclusive. Passing the same date for from and to returns 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, and rows.
  • When hasMore is true, pass nextCursor as cursor on 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())