Real-Time Streaming

Live market data over Server-Sent Events (SSE) — per-venue bid/ask quotes, 1-minute candles as they close, and raw trade ticks. One long-lived HTTP connection replaces polling, so a depth panel or live chart stays current without burning a call per refresh.

What's inside#

  • Per-venue BBO quotes — best bid/offer with sizes and the venue that quoted them, for symbols you warm. This is the same live quote source the trading platform's depth panel consumes.
  • 1-minute bars — every US-listed ticker, published within ~1s of each candle closing. No subscribe step: any symbol is already flowing.
  • Trade ticks — individual prints for warmed symbols.

All three are plain SSE (text/event-stream) — any HTTP client, EventSource, or curl works. No WebSocket, no SDK required.

Access#

3 credits per request. Starter plan or higher (quant_signals product group). Streaming is not included on the Free plan — a Free key gets 402 plan_required.

You are billed once per connection, not per frame — a connection that stays open all session costs 3 credits total. A request that fails (for example a 422 for a bad symbols=) is not charged: the charge is applied when the key is verified, then refunded automatically because no stream was delivered.

Endpoints#

MethodEndpointDescription
GET/api/v3/stream/quotes?symbols=Per-venue BBO quote stream. symbols= is required (comma-separated, max 50). Connecting auto-warms the symbols.
POST/api/v3/stream/quotes/subscribePre-warm symbols before connecting, so the first frames carry data instead of paying subscribe latency.
GET/api/v3/stream/bars?symbols=Live 1-minute OHLCV candles. symbols= is an optional filter — the feed carries every US-listed ticker.
GET/api/v3/stream/ticks?symbols=Live trade prints for warmed symbols.

When data flows#

Quotes, bars, and ticks are live roughly 04:00–20:00 ET (US pre-market through post-market), Monday–Friday — the window the capability manifest advertises for these tools. Outside that window — overnight, weekends, market holidays — the connection stays open and emits heartbeat frames but no market data.

A quiet stream outside market hours is correct, not broken. Don't treat the absence of quote frames as a connection failure; watch for heartbeat to confirm the stream is alive.

Frame format#

Every frame is an SSE event with an event name and a JSON data line. Market-data frames are wrapped in an envelope — the fields you want are nested under .data:

Code
1event: quotes2id: 1754094512345-03data: {"stream":"firm:stream:quotes","id":"1754094512345-0","data":{ ...frame fields... }}

Three control events are emitted alongside the data:

EventMeaning
quotes_warm(quotes only) which requested symbols were warmed, which were rejected, and how long the warm lasts
readythe stream is subscribed and open — sent immediately on connect
heartbeatidle keep-alive so proxies don't time out a quiet feed

Quote frame fields#

Inside .data: symbol, timestamp, type: "quote", bid_price, bid_size, ask_price, ask_size, bid_exchange, ask_exchange.

Sizes and venue IDs are the upstream feed's numbers passed through untransformed — bid_exchange / ask_exchange are numeric venue identifiers, not names.

Bar frame fields#

Inside .data: symbol, timestamp (bar close, ISO), type: "bar", open, high, low, close, volume, vwap, bar_period_s: 60.

For chart history, use the REST aggregate endpoints — a stream only carries candles from the moment you connect.

Examples#

Bash: open a quote stream#

Shell
1curl -N -H "X-API-Key: $TENGU_API_KEY" \2  "https://firm.tengu.co/api/v3/stream/quotes?symbols=AAPL,MSFT"

Live output on connect (captured outside market hours — note the heartbeats where quote frames would otherwise appear):

Code
1event: quotes_warm2data: {"warmed":["AAPL","MSFT"],"rejected":[],"resubscribe_within_s":300.0}3 4event: ready5data: {"streams":["firm:stream:quotes"],"symbol_filter":["AAPL","MSFT"],"redis_available":true,"universe_warm":null}6 7event: heartbeat8data: {"ts":278.136}

resubscribe_within_s is the warm TTL. You do not need to act on it while the connection is open — the server re-warms your symbols automatically for as long as you stay connected.

Bash: pre-warm before connecting#

Shell
1curl -X POST "https://firm.tengu.co/api/v3/stream/quotes/subscribe" \2  -H "X-API-Key: $TENGU_API_KEY" \3  -H "Content-Type: application/json" \4  -d '{"symbols": ["AAPL", "NVDA"], "ttl_s": 300}'
JSON
1{2  "ok": true,3  "timestamp": "2026-08-02T01:19:39.205411+00:00",4  "warmed": ["AAPL", "NVDA"],5  "rejected": [],6  "ttl_s": 300.0,7  "note": "daemon reconciles within ~5s; first frames follow on GET /api/v3/stream/quotes?symbols=... (market hours only — a quiet stream outside sessions is correct, not broken)",8  "redis_available": true9}

Idempotent — call again to extend the TTL. ttl_s is clamped to 30–3600 seconds.

Bash: live 1-minute candles (no subscribe step)#

Shell
1curl -N -H "X-API-Key: $TENGU_API_KEY" \2  "https://firm.tengu.co/api/v3/stream/bars?symbols=AAPL"
Code
1event: ready2data: {"streams":["firm:stream:bars"],"symbol_filter":["AAPL"],"redis_available":true,"universe_warm":null}3 4event: heartbeat5data: {"ts":325.231}

Omit ?symbols= entirely to receive candles for every US-listed ticker.

Python: consume quotes#

Python
1import json2import httpx3 4url = "https://firm.tengu.co/api/v3/stream/quotes"5headers = {"X-API-Key": "tengu_YOUR_KEY"}6 7with httpx.stream("GET", url, params={"symbols": "AAPL,MSFT"},8                  headers=headers, timeout=None) as r:9    r.raise_for_status()10    event = None11    for line in r.iter_lines():12        if line.startswith("event:"):13            event = line.split(":", 1)[1].strip()14        elif line.startswith("data:"):15            payload = json.loads(line.split(":", 1)[1].strip())16            if event == "heartbeat":17                continue                      # quiet feed, still alive18            if event == "quotes_warm":19                print("warmed:", payload["warmed"])20                continue21            frame = payload.get("data", payload)   # unwrap the envelope22            if frame.get("type") == "quote":23                print(frame["symbol"], frame["bid_price"], frame["ask_price"])

JavaScript: EventSource#

The quote stream reports its warm state both ways: an X-Firm-Quotes-Warmed response header (e.g. AAPL,MSFT, sorted) and the in-band quotes_warm event. Non-browser clients can read the header; EventSource cannot see response headers, which is why the same information is also delivered as an event.

JavaScript
1// Browser EventSource cannot send X-API-Key; proxy the stream through your2// own backend (which adds the header) and connect to that.3const es = new EventSource("/api/proxy/stream/quotes?symbols=AAPL,MSFT");4 5es.addEventListener("ready", (e) => console.log("open:", JSON.parse(e.data)));6es.addEventListener("heartbeat", () => {});          // ignore keep-alives7es.addEventListener("quotes", (e) => {8  const { data } = JSON.parse(e.data);               // unwrap9  console.log(data.symbol, data.bid_price, data.ask_price);10});

Limits & errors#

ConditionResponse
Free plan402 plan_required
More than 50 symbols on /stream/quotes422max 50 symbols per quote stream
/stream/quotes with symbols= omitted entirely422validation, detail: "query.symbols" (required-parameter check)
/stream/quotes with a blank symbols= (?symbols=, ?symbols=,,)422symbols_required (there is no all-market quote feed)
No requested symbol is warmable422no_valid_symbols

Quotes are fanned out per requested symbol, which is why symbols= is mandatory there but optional on bars. The two rejection codes differ: omitting the parameter fails the required-parameter check (validation), while supplying it empty fails the ticker check (symbols_required). Branch on the 422 status, not on error === "symbols_required" alone.

Next steps#