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:

FieldMeaning
topicFull MQTT topic the device published to.
payloadMessage body. UTF-8 if it's text; base64 if it isn't (then is_base64: true).
is_base64true only when the payload couldn't be safely sent as UTF-8 string.
tsUnix seconds when the api service received the message.
retainedThe MQTT retained bit on the original publish.
org_idThe 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 paramWhat 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:

  1. Auth via an HTTP-style API key, not mTLS or a broker password — works from anywhere standard HTTPS goes.
  2. Org scoping is enforced server-side — you can't accidentally see another org's traffic, even if you guess a topic.
  3. 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 / statusMeaning
401 on the upgrade responseBad / missing API key.
429 on the upgrade responseRate limit (the WebSocket opens against the API rate limit; once open, individual messages do not count).
WebSocket close 1011Server error — usually broker connection lost. Reconnect.
WebSocket close 1000Clean close by either side.

Operationally

  • Connections survive idle — no Cloudflare timeout because api.scadable.com is grey-cloud.
  • A /healthz request to api.scadable.com returns 200 ok regardless of broker state, so health checks should also probe the WebSocket upgrade if you want full-stack monitoring.