MQTT stream (WebSocket)
A single WebSocket connection gives you every MQTT message published by your org's devices, in real time, with millisecond-class fan-out from the broker.
wss://api.scadable.com/v1/mqtt/stream
Connect
Authentication is the same scd_live_… API key as the rest of the API. Two ways to send it on the WebSocket upgrade:
# Preferred: Authorization header (any non-browser client).
websocat -H "Authorization: Bearer scd_live_..." \
"wss://api.scadable.com/v1/mqtt/stream"
# Browser fallback (WebSocket can't set Authorization at upgrade):
new WebSocket("wss://api.scadable.com/v1/mqtt/stream?token=scd_live_...")Either works. The query-string version logs into the audit log with the token redacted as *** so accidental URL leaks don't leak the key.
What you receive
Every MQTT message arriving at the broker from devices in your org becomes one JSON frame:
{
"topic": "scadable/SC-a3f8b2c4e6d8/heartbeat",
"payload": "{\"uptime_s\":1234,\"free_heap\":187392}",
"is_base64": false,
"ts": 1748678430,
"retained": false,
"org_id": "org_..."
}Fields:
| Field | Meaning |
|---|---|
topic | Full MQTT topic the device published to. |
payload | Message body. UTF-8 if it's text; base64 if it isn't (then is_base64: true). |
is_base64 | true only when the payload couldn't be safely sent as UTF-8 string. |
ts | Unix seconds when the api service received the message. |
retained | The MQTT retained bit on the original publish. |
org_id | The org the source device belongs to. Always equals your key's org. |
Filter what you receive
Without filters you get everything from your org — heartbeats, ota/status, your own custom topics, etc. Three optional filters:
| Query param | What it does |
|---|---|
?ns=<ns_id> | Only messages from devices in that namespace. |
?device=<cn> | Only messages from one device (e.g. SC-a3f8b2c4e6d8). |
?topic=<glob> | MQTT-style topic match. + matches one segment, # matches the tail. |
Examples:
# Just heartbeats from any device
wss://api.scadable.com/v1/mqtt/stream?topic=scadable/%2B/heartbeat
# Everything from one device
wss://api.scadable.com/v1/mqtt/stream?device=SC-a3f8b2c4e6d8
# Everything within one namespace
wss://api.scadable.com/v1/mqtt/stream?ns=ns_abcd1234
# Combine — heartbeats from one namespace only
wss://api.scadable.com/v1/mqtt/stream?ns=ns_abcd1234&topic=scadable/%2B/heartbeat(+ URL-encodes to %2B.)
Backpressure
The api service buffers up to 256 frames per WebSocket subscriber. If your client falls behind that, the server drops frames (oldest first) rather than blocking. Slow consumers should drop, not stall — the rest of the fleet shouldn't pay for one client's downstream slowness.
If you need lossless delivery, consume from the broker side directly via your own MQTT client (talk to us about bridge access for high-throughput integrations).
Reconnect
WebSocket connections can drop for any of the usual reasons (network blip, browser tab sleep, server restart). Your client should reconnect with exponential backoff. The server does not replay missed messages — there's no concept of resumable streaming for v1.
Practical sketch (Node):
function connect() {
const ws = new WebSocket("wss://api.scadable.com/v1/mqtt/stream?token=scd_live_...");
ws.onmessage = (e) => handleFrame(JSON.parse(e.data));
ws.onclose = () => setTimeout(connect, jitter(1000, 30000));
}
connect();Doesn't this just expose MQTT?
It exposes the contents of your org's MQTT topics, with three usability wins over running your own MQTT client:
- Auth via an HTTP-style API key, not mTLS or a broker password — works from anywhere standard HTTPS goes.
- Org scoping is enforced server-side — you can't accidentally see another org's traffic, even if you guess a topic.
- No broker IPs or ports to configure — the api service hides that detail and continues to work if we move brokers.
If you have a use case for raw MQTT (high-throughput ingestion, exact QoS-2 semantics, custom publishers), reach out — we can set up dedicated broker creds. The WebSocket stream is the right default for ~everyone.
Errors
| Close code / status | Meaning |
|---|---|
401 on the upgrade response | Bad / missing API key. |
429 on the upgrade response | Rate limit (the WebSocket opens against the API rate limit; once open, individual messages do not count). |
WebSocket close 1011 | Server error — usually broker connection lost. Reconnect. |
WebSocket close 1000 | Clean close by either side. |
Operationally
- Connections survive idle — no Cloudflare timeout because
api.scadable.comis grey-cloud. - A
/healthzrequest toapi.scadable.comreturns200 okregardless of broker state, so health checks should also probe the WebSocket upgrade if you want full-stack monitoring.
Updated 10 days ago
