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#
| Method | Endpoint | Description |
|---|---|---|
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/subscribe | Pre-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:
1event: quotes2id: 1754094512345-03data: {"stream":"firm:stream:quotes","id":"1754094512345-0","data":{ ...frame fields... }}Three control events are emitted alongside the data:
| Event | Meaning |
|---|---|
quotes_warm | (quotes only) which requested symbols were warmed, which were rejected, and how long the warm lasts |
ready | the stream is subscribed and open — sent immediately on connect |
heartbeat | idle 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#
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):
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#
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}'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)#
1curl -N -H "X-API-Key: $TENGU_API_KEY" \2 "https://firm.tengu.co/api/v3/stream/bars?symbols=AAPL"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#
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.
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#
| Condition | Response |
|---|---|
| Free plan | 402 plan_required |
More than 50 symbols on /stream/quotes | 422 — max 50 symbols per quote stream |
/stream/quotes with symbols= omitted entirely | 422 — validation, detail: "query.symbols" (required-parameter check) |
/stream/quotes with a blank symbols= (?symbols=, ?symbols=,,) | 422 — symbols_required (there is no all-market quote feed) |
| No requested symbol is warmable | 422 — no_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#
- Quant Signals — the product group streaming belongs to.
- Market Data — REST quotes, OHLCV history, and reference data.
- Pricing & credits — plan access and what each call costs.