Products
Real-Time Streaming
Live market data and model output over Server-Sent Events (SSE): top-of-book quotes (NBBO), 1-minute candles as they close, trade ticks, fused signals, decision events and structured news, plus a freshness-labelled market-event contract. One long-lived HTTP connection replaces polling, so a quote panel, live chart or agent stays current without paying for a call per refresh. Not every feed has a producer running today: When data flows says which ones carry data.
What's inside
- Top-of-book quotes (NBBO): the consolidated best bid and best ask, with their sizes and the ID of the venue quoting each side, for the symbols you request. This is not order-book depth: each frame carries one best bid and one best ask, with no per-venue book and no L2 ladder. Connecting with
symbols=queues coverage for those names, and the first quote frames follow within about 5 seconds. - 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. A connect with
symbols=queues coverage for those names; the producer picks it up asynchronously, with a target of about 5 seconds. - Fused signals and decisions: the model's live
long/short/holdsignals, and thebuy/selldecisions that pass its confidence, strength and cooldown gates. No producer publishes to these feeds today; see When data flows. - Structured news: articles with sentiment, topics and tickers. No producer publishes to this feed today; see When data flows.
- Market events: a freshness-labelled event contract covering price and volatility breaches, news salience, 8-K and Form 4 filings and FX moves, across equities, crypto and FX. Every event carries its source time, feed lag and a staleness flag. No detector publishes to this feed yet; see When data flows.
- Catch-up and a guide: a JSON page of the market events you missed while disconnected, and a JSON guide that maps real-time questions to the right feed.
Every stream is plain SSE (text/event-stream): any HTTP client, EventSource, or curl works. No WebSocket, no SDK required.
The nine SSE routes are REST only: they are not MCP tools, and an MCP client cannot open them. The two JSON routes, the guide (stream_realtime_guide) and the catch-up page (market_events_catchup), are MCP tools.
Access
3 credits per connection. Starter plan or higher (quant_signals product group) for every /api/v3/stream/* route. 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 rejected with an error status (for example a 422 for a bad symbols=, or a 503 when quote coverage cannot be requested) is not charged: the charge is taken when the key is verified and refunded automatically because of the error status.
A feed whose producer is not running is refused before it opens: 503 upstream_, not billed (today that is /stream/signals, /stream/decisions, /stream/news, /stream/alerts and /stream/market_events; see Which feeds carry data today). A stream that opens returns HTTP 200 and is billed, even when it has nothing to deliver. That includes a connection whose ready frame says ready: false and that then closes with a redis_unavailable error, so reconnect with backoff rather than in a tight loop: each attempt costs 3 credits. An empty /api/v3/events page after a real since cursor is a billed 200 meaning no new events. When the event store cannot be read, or the stream holds no events at all, /api/v3/events answers 503 instead, not billed (see Catch-up refusals).
Two related routes are billed as other products:
| Route | Product | Credits | Minimum plan |
|---|---|---|---|
/api/v3/stream/* (every route in the tables below except /api/v3/events) | quant_signals | 3 | Starter |
/api/v3/events (market-event catch-up) | events_transcripts | 3 | Pro |
/api/ (earnings consensus as SSE) | macro_rates | 2 | Starter |
The earnings stream belongs to Macro, Rates & FX. It delivers the same consensus contract as /api/, as a fast partial event followed by a verified result event.
Endpoints
Market data streams
| Method | Endpoint | MCP tool | Description |
|---|---|---|---|
GET | /api/ | REST only | Top-of-book (NBBO) quote stream: best bid and best ask with their sizes and each side's venue ID. Not order-book depth. symbols= is required (comma-separated, max 50). Connecting queues coverage for the symbols and keeps it refreshed while you stay connected. |
GET | /api/v3/stream/bars?symbols= | REST only | Live 1-minute OHLCV candles. symbols= is an optional filter: the feed carries every US-listed ticker. |
GET | /api/v3/stream/ticks?symbols= | REST only | Live US-equity trade prints. symbols= is an optional filter; a filtered connect queues coverage for those names. |
Model, news and event streams
| Method | Endpoint | MCP tool | Description |
|---|---|---|---|
GET | /api/ | REST only | Fused signals: long, short or hold with strength and model confidence. symbols= optional. Unavailable right now: no producer publishes to this feed yet, so the request is refused with 503 upstream_ before any stream opens, and it is not billed. It goes live when its producer ships. |
GET | /api/ | REST only | Decision-lifecycle events: signals after the confidence, strength and cooldown gates, strictly buy or sell. symbols= optional. Unavailable right now: no producer publishes to this feed yet, so the request is refused with 503 upstream_ before any stream opens, and it is not billed. It goes live when its producer ships. |
GET | /api/v3/stream/news?symbols= | REST only | Structured news items with sentiment, topics and tickers. symbols= optional. Unavailable right now: no producer publishes to this feed yet, so the request is refused with 503 upstream_ before any stream opens, and it is not billed. It goes live when its producer ships. |
GET | /api/v3/stream/alerts | REST only | Alerts stream. Takes no parameters. Unavailable right now: no producer publishes to this feed yet, so the request is refused with 503 upstream_ before any stream opens, and it is not billed. It goes live when its producer ships. Its events are not a published contract yet. |
GET | /api/ | REST only | Freshness-labelled market events across equities, crypto and FX. symbols= optional. Stale events are flagged, never dropped. Unavailable right now: no producer publishes to this feed yet, so the request is refused with 503 upstream_ before any stream opens, and it is not billed. It goes live when its producer ships. |
GET | /api/ | REST only | Per-user notification stream. user_id is required. The stream is scoped to the calling key: a key reads only its own namespace, whatever user_id it passes, and a request with no authenticated key is refused with 403. Nothing publishes to this stream yet, so an open connection carries only control frames and is still billed as a connection. Don't build on it until this page documents its events. |
Catch-up and agent guide (JSON)
| Method | Endpoint | MCP tool | Description |
|---|---|---|---|
GET | /api/v3/events | market_events_catchup | A finite page of retained market events after a stream id, for use after a reconnect. ?since= (string, optional: the exclusive stream id to read after, written <ms> or <ms>-<seq>, or - for the start; omit it to start at the oldest retained event; any other form returns 422), ?symbols= (comma-separated, optional), ?limit= (integer, optional, 1-1000, default 200: the number of retained events read before the symbols= filter is applied). Returns {events, count, next_; feed.last_event_age_s is the age in seconds of the newest event on the stream, any symbol (null when the stream is empty or could not be timed). See paging rules. Billed as events_transcripts (Pro, 3 credits). |
GET | /api/v3/stream/realtime_ | stream_realtime_guide | A JSON protocol for agents: which feed answers which real-time intent, payload shapes, when to prefer SSE over REST, and fallback rules. Takes no parameters. |
When data flows
Quotes, bars, and ticks are live roughly 04:00-20:00 ET (US pre-market through post-market), Monday to 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.
The market-events feed and its catch-up page are a published contract with no producer yet: no detector publishes market events today, so /stream/market_events is refused with 503 upstream_ (not billed), and /api/v3/events read from the start of a stream that holds no events answers 503 (see Catch-up refusals). The field list under Market event fields is the contract each event follows once one is published. Nothing holds events back by market hours; instead each event carries tradeable_now, which is false when its market was closed at detection, so an overnight equity event can be queued rather than acted on.
Which feeds carry data today. Quotes, bars and ticks have a producer running. No producer publishes to /stream/signals, /stream/decisions, /stream/news, /stream/alerts or /stream/market_events yet: each is refused with 503 upstream_ before it opens and is not billed, and each goes live when its producer ships. /stream/events (per-user notifications) opens, but nothing publishes to it yet, so it carries only control frames (ready, then heartbeat) and is billed as a connection. The field lists below are the contract each feed follows once its producer runs. Until then, read signals and news over REST: see Quant Signals and News & Sentiment.
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:
event: quotesid: 1754094512345-0data: {"stream":"firm:stream:quotes","id":"1754094512345-0","data":{ ...frame fields... }}The event name is the stream's name: quotes, bars, ticks, signals, decisions, news, alerts or market_events. The id: line is the stream id of that frame; pass it as since to /api/v3/events to catch up on market events after a reconnect.
Four control events are emitted alongside the data:
| Event | Meaning |
|---|---|
quotes_warm | (quotes only) which requested symbols were warmed, which were rejected, which did not fit in live-quote capacity (capacity_not_admitted), and how long the warm lasts |
ready | the stream's first frame, sent immediately on connect, except on /stream/quotes, where quotes_warm comes first and ready follows it. Check its ready field: true means the stream is open; false means the live transport is unavailable, and an error event follows |
error | sent after ready: false with error: "redis_, a message and a reason. The server then closes the stream; reconnect with backoff |
heartbeat | idle keep-alive so proxies don't time out a quiet feed |
The ready frame carries ready, streams, symbol_filter, redis_available, redis_, transport (the backend probe: status, reachable, latency_ms, reason) and universe_warm. universe_warm is null when you pass no symbols= or the transport is down; otherwise it reports the coverage request (request_status, subscription_requested, rejected, capacity_not_admitted and related fields). Treat a ready frame as success only when ready is true.
ready: true describes the transport only. Whether anything will publish is in producers, one entry per stream with status, live, heartbeat_age_s and last_event_age_s (the age of the newest event on that stream), and in producer_live: true when every producer is known to be running, false when one is known to be down, and null when that cannot be observed for some stream. Both are null when the transport is down.
Quote frame fields
Inside .data: symbol, timestamp, type: "quote", bid_price, bid_size, ask_price, ask_size, bid_exchange, ask_exchange, size_unit, normal_round_lot_shares.
Each frame is the consolidated best bid and best ask (NBBO) for the symbol, not order-book depth.
Sizes are round lots, not shares. Every frame says so: size_unit is "vendor_ and normal_round_lot_shares is 100. A bid_size of 3 is 3 round lots, normally 300 shares, so never display bid_size or ask_size as a share count. Sizes and venue IDs are passed through from the upstream feed untransformed: bid_exchange and ask_exchange are numeric venue IDs sent as strings (for example "11"), not venue 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.
Tick frame fields
Inside .data: symbol, timestamp, type: "trade", price, size.
Signal and decision frame fields
Signals, inside .data: symbol, timestamp (when the signal was generated, ISO 8601), signal_type (long, short or hold), strength (1-4), entry_price, stop_loss, take_profit and position_size (always null on this feed), model_confidence (0-1), model_name, meta.
Decisions, inside .data: symbol, timestamp (when the decision was made), action (buy or sell), strength, entry_price, confidence, source_signal_model, source_signal_ts (the timestamp of the signal it came from), reason, meta.
Use these timestamps rather than the time a frame arrives.
Both are model output for research, like the rest of Quant Signals. A buy decision is not trade authorization.
News frame fields
News, inside .data: symbol, headline, summary, url, source, sentiment, topics, tickers, meta.
Market event fields
Inside .data:
| Field | Meaning |
|---|---|
event_id | Unique id of the event |
symbol, asset_class | The instrument; asset_class is equity, crypto or fx |
event_class | price_breach, vol_breach, news_salience, filing_8k, filing_form4 or fx_move |
magnitude, salience | Size of the move and how notable it is |
as_of | The source's own timestamp: when the data was true, not when it was read |
detected_at | When the event was detected |
feed_lag_ms | detected_at minus as_of, derived server-side |
staleness | fresh, stale or unknown (no source timestamp, or one in the future) |
tradeable_now | Whether the event's market is open at detection |
baseline_as_of | The source timestamp the breach was measured against |
detail | Event-specific detail |
A stale event is still published, with staleness: "stale", so a feed outage never reads as a calm market. Down-weight stale and unknown events rather than dropping them silently.
Examples
Bash: open a quote stream
curl -N -H "Authorization: Bearer $WEALTHNOW_API_KEY" \ "https://firm.wealthnow.io/api/v3/stream/quotes?symbols=AAPL,MSFT"Output on connect, illustrative and abbreviated (... marks omitted fields). Outside market hours, heartbeats appear where quote frames would otherwise be:
event: quotes_warmdata: {"warmed":["AAPL","MSFT"],"rejected":[],"capacity_not_admitted":[],"resubscribe_within_s":300.0} event: readydata: {"ready":true,"streams":["firm:stream:quotes"],"symbol_filter":["AAPL","MSFT"],"redis_available":true,"redis_client_library_available":true,"transport":{"status":"ready","reachable":true,...},"universe_warm":{"request_status":"ok","subscription_requested":["AAPL","MSFT"],"rejected":[],"capacity_not_admitted":[],...}} event: heartbeatdata: {"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. Connecting is what requests coverage, so no separate call is needed. Symbols listed in capacity_not_admitted were valid but did not fit in live-quote capacity, so they may not stream on this connection.
Bash: live 1-minute candles (no subscribe step)
curl -N -H "Authorization: Bearer $WEALTHNOW_API_KEY" \ "https://firm.wealthnow.io/api/v3/stream/bars?symbols=AAPL"event: readydata: {"ready":true,"streams":["firm:stream:bars"],"symbol_filter":["AAPL"],"redis_available":true,"redis_client_library_available":true,"transport":{...},"universe_warm":{"request_status":"ok","subscription_requested":["AAPL"],...}} event: heartbeatdata: {"ts":325.231}Omit ?symbols= entirely to receive candles for every US-listed ticker.
Bash: fused signals for a few names
curl -N -H "Authorization: Bearer $WEALTHNOW_API_KEY" \ "https://firm.wealthnow.io/api/v3/stream/signals?symbols=AAPL,NVDA"No producer publishes signals today, so this connection currently carries only ready and heartbeat frames (see When data flows). Illustrative signal frame for when signals are published (the values are placeholders, not a live sample):
event: signalsid: 1790259900121-0data: {"stream":"firm:stream:signals","id":"1790259900121-0","data":{"symbol":"NVDA","timestamp":"2026-09-24T14:25:00.118342+00:00","signal_type":"long","strength":2,"entry_price":181.4,"stop_loss":null,"take_profit":null,"position_size":null,"model_confidence":0.61,"model_name":"stream-ensemble-v1","meta":{}}}/api/v3/stream/decisions takes the same symbols= filter and emits decisions frames with the decision fields listed above.
Bash: market events, then catch up after a reconnect
Keep the id: of the last frame you processed. After a disconnect, page through what you missed with /api/v3/events, then reopen the stream.
# 1. Live feed (Starter, 3 credits per connection)curl -N -H "Authorization: Bearer $WEALTHNOW_API_KEY" \ "https://firm.wealthnow.io/api/v3/stream/market_events?symbols=AAPL,BTC" # 2. After a reconnect: everything after the last id you saw (Pro, 3 credits per call)curl -H "Authorization: Bearer $WEALTHNOW_API_KEY" \ "https://firm.wealthnow.io/api/v3/events?since=1758721500123-0&symbols=AAPL,BTC&limit=200"No detector publishes market events yet, so while the stream holds no events a read from its start is refused with 503 (see Catch-up refusals). Illustrative response once events are published, not a live sample:
{ "ok": true, "timestamp": "2026-09-24T14:12:00Z", "events": [ { "id": "1758721562001-0", "event_id": "5f0c3a9e2b7d4e1f8a6c0d2b4e6f8a1c", "symbol": "BTC", "asset_class": "crypto", "event_class": "price_breach", "magnitude": 0.031, "salience": 0.7, "as_of": "2026-09-24T14:05:58+00:00", "detected_at": "2026-09-24T14:06:02+00:00", "feed_lag_ms": 4000, "staleness": "fresh", "tradeable_now": true, "baseline_as_of": "2026-09-24T14:04:58+00:00", "detail": {} } ], "count": 1, "next_since": "1758721562001-0", "stream": "firm:stream:market_events", "feed": { "last_event_age_s": 12.4, "producer_heartbeat": "not_instrumented", "note": "..." }}Page with these rules:
- Call again with
since=set tonext_sinceuntilnext_sincecomes back equal to thesinceyou sent. That means no retained events were left to read. Then reopen the SSE connection. - Don't stop on
count: 0.limitcounts the raw events read before yoursymbols=filter is applied, so a page can hold no matching events while later matching events remain.next_sincestill moves past the events the filter skipped, so you never re-read them. - A
200with an empty page and an unchangednext_sincemeans you are caught up. A store that cannot be read is a503, never an empty page. Readfeed.last_event_age_sto see how long the whole stream has been quiet. - Omit
since, or passsince=-, to start at the oldest retained event.
Each event keeps its original as_of, feed_lag_ms and staleness, so an event you missed while offline still shows its true age.
Catch-up refusals
/api/v3/events answers 503 with error: "upstream_, not billed, instead of an empty page in two cases:
- The event store cannot be read:
error_,code: "redis_ unavailable" Retry-After: 5. - A read from the start (
sinceomitted or-) finds a stream that holds no events at all, because nothing is publishing to it:error_withcode: "dataset_ unavailable" reason: "table_empty"andRetry-After: 60. An empty page after a real cursor, or on a stream whose newest event can be timed, stays a200.
A malformed since returns 422 validation.
Bash: the real-time guide for agents
curl -H "Authorization: Bearer $WEALTHNOW_API_KEY" \ "https://firm.wealthnow.io/api/v3/stream/realtime_guide"Illustrative excerpt:
{ "ok": true, "timestamp": "2026-09-24T14:13:00Z", "version": "2.0", "principle": "For any 'live', 'right now', 'current price', ... style question ...", "endpoints": [ { "path": "/api/v3/stream/bars", "query": "symbols=AAPL,MSFT (optional)", "when_to_use": "Closed 1-minute OHLCV candles across active US-listed equities; no warm step required.", "latency_ms": "published at each 1m bar close", "payload_shape": "{ symbol, timestamp, type:'bar', open, high, low, close, volume, vwap, bar_period_s:60 }" } ], "agent_protocol": ["..."], "fallback_when_flags_off": "..."}The guide is written for agents and also names internal status and fallback routes. If a route it names is not in the API reference, it is not available to your key; use the routes on this page and the REST products instead.
Python: consume quotes
import jsonimport osimport httpx url = "https://firm.wealthnow.io/api/v3/stream/quotes"headers = {"Authorization": f"Bearer {os.environ['WEALTHNOW_API_KEY']}"} with httpx.stream("GET", url, params={"symbols": "AAPL,MSFT"}, headers=headers, timeout=None) as r: r.raise_for_status() event = None for line in r.iter_lines(): if line.startswith("event:"): event = line.split(":", 1)[1].strip() elif line.startswith("data:"): payload = json.loads(line.split(":", 1)[1].strip()) if event == "heartbeat": continue # quiet feed, still alive if event == "ready" and not payload.get("ready"): continue # transport down: an error event follows if event == "error": raise RuntimeError(payload.get("error")) # the server closes the stream if event == "quotes_warm": print("warmed:", payload["warmed"]) continue frame = payload.get("data", payload) # unwrap the envelope if frame.get("type") == "quote": 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.
// Browser EventSource cannot send an Authorization header; proxy the stream// through your own backend (which adds the header) and connect to that.const es = new EventSource("/api/proxy/stream/quotes?symbols=AAPL,MSFT"); es.addEventListener("ready", (e) => { const ready = JSON.parse(e.data); if (!ready.ready) console.warn("transport unavailable; the server will close this stream");});es.addEventListener("heartbeat", () => {}); // ignore keep-aliveses.addEventListener("quotes", (e) => { const { data } = JSON.parse(e.data); // unwrap console.log(data.symbol, data.bid_price, data.ask_price);});Limits & errors
| Condition | Response |
|---|---|
| Free plan | 402 plan_required |
/api/v3/events on Starter | 402 plan_required (the catch-up page is Pro) |
More than 50 symbols on /stream/quotes | 422 too_many_symbols (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 |
/stream/events without user_id | 422 required-parameter check |
/stream/events with no authenticated key | 403 |
/api/v3/events with a malformed since | 422 validation |
/api/v3/events when the event store cannot be read | 503 upstream_unavailable, error_, Retry-After: 5 |
/api/v3/events from the start of a stream that holds no events | 503 upstream_unavailable, error_, Retry-After: 60 |
| Live transport unavailable at connect | HTTP 200: a ready frame with ready: false, then an error event (redis_unavailable); the server closes the stream. Billed, because it is not an error status |
| Quote coverage cannot be requested right now | 503 quote_warm_unavailable; retry without changing the symbols |
| Quote coverage is at capacity | 503 quote_; retry after active requests expire |
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_ alone. Requests rejected with an error status, including the 422s and 503s above, are not charged. The ready: false case is an HTTP 200 and is charged.