Skip to content

Real-time signals (SSE)

GET /api/v1/signals/stream is a Server-Sent Events (SSE) stream that pushes new Oracle signals to your client the moment they are generated — no polling required.

Oracle signals are machine-generated trading signals derived from the platform’s data: news sentiment, price movements, volume anomalies, technical indicators, SEC filings, and prediction-market activity. Each signal carries a type, a target entity (asset, market, or prediction market), a strength (0–100), and a direction (from VERY_BULLISH to VERY_BEARISH).

The stream delivers signals as they are created. To browse historical signals or fetch a single one with full metadata, use the REST endpoints under /signals instead — see concepts: signals.

RequirementDetail
AuthenticationRequired — send Authorization: Bearer mtk_your_key_here. A key is mandatory on this endpoint (the demo key mtk_demo works).
PlanStarter, Professional, Enterprise, or Custom (or the demo key). Free is blocked.
Concurrent connectionsCapped per plan by maxWebSocketConnections: Free 0, Starter 1, Professional 5, Enterprise 50, Custom 100. Exceeding the cap returns a 429 error event. The field name is legacy; Client API streaming uses SSE.
CreditsThe stream-open request costs 0 credits. The first prepaid billing period is charged before the open event is sent, then each additional elapsed period is charged at the start of that period. If billing cannot be confirmed, the stream does not open or continue. Default: 1 credit per 30 minutes.

See Plans & credits for the full table.

GET /api/v1/signals/stream
Authorization: Bearer mtk_your_key_here
Accept: text/event-stream

Both filters are applied server-side, so you only receive (and only get billed network for) the signals you care about.

ParameterTypeDescription
entityTypestringOnly stream signals for this entity type — ASSET, MARKET, or PREDICTION_MARKET.
minStrengthnumberOnly stream signals with signalStrength at or above this value (0–100).

The stream emits four named event types. Every message is standard text/event-stream: an event: line, a data: line (JSON), and optionally an id: line.

Sent once, immediately after the stream is established, so clients know the subscription is live.

event: open
data: {"message":"Subscribed to Oracle signals","timestamp":"2026-06-05T12:00:00.000Z"}

A new Oracle signal. The SSE id: line is set to the signal’s id, so a reconnecting client can send Last-Event-ID (see Reconnection).

id: 9f1c2d3e-4a5b-6c7d-8e9f-0a1b2c3d4e5f
event: signal
data: {"id":"9f1c2d3e-4a5b-6c7d-8e9f-0a1b2c3d4e5f","signalType":"VOLUME","entityType":"ASSET","entityId":"3b2a1c4d-...","signalStrength":82,"signalDirection":"BULLISH","signalAt":"2026-06-05T12:00:03.000Z"}

The data payload for a signal event contains exactly these fields:

FieldTypeDescription
idstring (UUID)Unique signal identifier. Also sent as the SSE id:.
signalTypestringSignal type — e.g. NEWS, VOLUME, PRICE, TECHNICAL.
entityTypestringTarget entity type — ASSET, MARKET, PREDICTION_MARKET, etc.
entityIdstring (UUID) | nullIdentifier of the target entity, or null.
signalStrengthnumberStrength from 0 to 100.
signalDirectionstringOne of VERY_BULLISH, BULLISH, SLIGHTLY_BULLISH, NEUTRAL, SLIGHTLY_BEARISH, BEARISH, VERY_BEARISH.
signalAtstring (ISO 8601) | nullWhen the signal condition occurred.

Sent roughly every 25 seconds when no signals are flowing, to keep the connection (and any intermediary proxy) from idling out.

event: heartbeat
data: {"timestamp":"2026-06-05T12:00:25.000Z"}

Sent when there’s an authentication, authorization, or connection-limit problem. The HTTP status is also set accordingly, and the stream then closes.

event: error
data: {"message":"Real-time signal streaming requires a Starter plan or higher. Upgrade your plan to access live signals.","code":"FORBIDDEN"}
HTTP statuscodeCause
401UNAUTHORIZEDNo API key provided. Streaming requires authentication.
403FORBIDDENPlan lacks real-time SSE access (e.g. Free). Upgrade to Starter+.
429RATE_LIMIT_EXCEEDEDAlready at the plan’s maxWebSocketConnections cap. Close an open stream first.

Use --no-buffer (-N) so curl flushes each event as it arrives instead of waiting for the whole response.

Terminal window
curl --no-buffer \
-H "Authorization: Bearer mtk_your_key_here" \
-H "Accept: text/event-stream" \
"https://api.quantconomy.com/api/v1/signals/stream?entityType=ASSET&minStrength=70"
const res = await fetch(
'https://api.quantconomy.com/api/v1/signals/stream?entityType=ASSET&minStrength=70',
{
headers: {
Authorization: 'Bearer mtk_your_key_here',
Accept: 'text/event-stream',
},
}
);
if (!res.ok || !res.body) {
throw new Error(`Stream failed: ${res.status}`);
}
const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
let buffer = '';
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += value;
// SSE events are separated by a blank line.
let boundary;
while ((boundary = buffer.indexOf('\n\n')) !== -1) {
const raw = buffer.slice(0, boundary);
buffer = buffer.slice(boundary + 2);
let event = 'message';
const dataLines = [];
for (const line of raw.split('\n')) {
if (line.startsWith('event:')) event = line.slice(6).trim();
else if (line.startsWith('data:')) dataLines.push(line.slice(5).trim());
// (id: lines are also present on `signal` events)
}
if (dataLines.length === 0) continue;
const data = JSON.parse(dataLines.join('\n'));
if (event === 'signal') {
console.log('Signal:', data.signalType, data.signalDirection, data.signalStrength);
} else if (event === 'error') {
console.error('Stream error:', data.code, data.message);
return;
}
}
}
import json
import httpx
url = "https://api.quantconomy.com/api/v1/signals/stream"
headers = {
"Authorization": "Bearer mtk_your_key_here",
"Accept": "text/event-stream",
}
params = {"entityType": "ASSET", "minStrength": 70}
with httpx.stream("GET", url, headers=headers, params=params, timeout=None) as res:
res.raise_for_status()
event = "message"
data_lines = []
for line in res.iter_lines():
if line == "": # blank line terminates an event
if data_lines:
data = json.loads("\n".join(data_lines))
if event == "signal":
print("Signal:", data["signalType"], data["signalDirection"], data["signalStrength"])
elif event == "error":
print("Stream error:", data["code"], data["message"])
break
event, data_lines = "message", []
elif line.startswith("event:"):
event = line[len("event:"):].strip()
elif line.startswith("data:"):
data_lines.append(line[len("data:"):].strip())

Long-lived streams will eventually drop — network blips, proxy timeouts, or a deploy. Build reconnection in.

  1. Distinguish fatal from transient. An error event with UNAUTHORIZED, FORBIDDEN, or RATE_LIMIT_EXCEEDED is fatal: do not auto-reconnect, fix the key/plan or close another stream first. A dropped connection with no error event is transient — reconnect.

  2. Back off exponentially with jitter. On a transient drop, wait, then retry, doubling the delay each time up to a ceiling (e.g. 1s → 2s → 4s … capped at ~30s) and adding random jitter to avoid thundering-herd reconnects.

  3. Watch the heartbeat. If you don’t see a signal or heartbeat within ~60s (heartbeats arrive about every 25s), treat the connection as dead and reconnect.

  4. Resume with Last-Event-ID. Each signal event carries its id as the SSE id:. Browser EventSource resends the last id automatically as the Last-Event-ID header on reconnect; with fetch/httpx, track the last signal id yourself and send it as the Last-Event-ID request header so you can de-duplicate. Treat the stream as at-least-once: dedupe by signal id.

  5. Respect the connection cap. Make sure the dropped stream is actually released before reconnecting — opening a new stream while the old slot is still held can trip the maxWebSocketConnections cap and return 429. Reconnect one stream at a time per account.