Skip to main content

WebSocket Integration

Overview​

Build real-time dashboards and monitoring tools by subscribing to the Haltless WebSocket. Once connected, you receive a live stream of new sensor readings as they're ingested and alerts as they fire, without polling. The stream covers the machines your user may see; alerts are included if your role can open the Alerts page.

URLwss://api.haltless.io/api/v1/ws/dashboard
AuthSend your JWT access token as the first message, within 4 seconds of connecting
Keep-aliveServer sends a heartbeat every 30 seconds
Your app connects to wss://api.haltless.io/api/v1/ws/dashboard and sends its access token as the first message, within 4 seconds or the socket closes with 4008. While the socket is open, Haltless sends a heartbeat every 30 seconds once your session checks out, a new_reading as each reading is stored, and a new_alert as each alert fires. When the token expires or the session ends, it closes with 4001: refresh the token, then reconnect.Your app connects to wss://api.haltless.io/api/v1/ws/dashboard and sends its access token as the first message, within 4 seconds or the socket closes with 4008. While the socket is open, Haltless sends a heartbeat every 30 seconds once your session checks out, a new_reading as each reading is stored, and a new_alert as each alert fires. When the token expires or the session ends, it closes with 4001: refresh the token, then reconnect.

Authentication​

The connection is authenticated with the same JWT access token you use for the REST API (see Authentication). To keep the token out of URLs and server logs, you send it as the first text message after the socket opens, not as a query parameter.

# Do NOT put the token in the URL
wss://api.haltless.io/api/v1/ws/dashboard?token=eyJhbG... ← don't do this

# DO send it as the first message in your onopen handler
ws.send(accessToken); ← do this

Send the token promptly. If you don't authenticate within 4 seconds of connecting, the server closes the socket with code 4008.

Quick start (JavaScript / TypeScript)​

const WS_URL = "wss://api.haltless.io/api/v1/ws/dashboard";

function connect(accessToken: string) {
const ws = new WebSocket(WS_URL);

ws.onopen = () => {
// Authenticate by sending the JWT as the very first message.
ws.send(accessToken);
};

ws.onmessage = (event) => {
const data = JSON.parse(event.data);

switch (data.type) {
case "heartbeat":
// Connection is alive.
break;
case "new_reading":
console.log(
`${data.machine_id}: ${data.metric_name} = ${data.value} ${data.unit}`
);
break;
case "new_alert":
console.log(`[${data.severity.toUpperCase()}] ${data.message}`);
break;
default:
// Ignore message types this client doesn't know.
break;
}
};

ws.onclose = (event) => {
console.log(`Disconnected: code=${event.code}`);
};

return ws;
}

connect("YOUR_JWT_ACCESS_TOKEN");

Message types​

Every message is a JSON object with a type field. Switch on type to handle each kind.

heartbeat​

Sent every 30 seconds to keep the connection alive and let you detect a stalled socket.

{
"type": "heartbeat",
"server_time": "2026-08-05T10:30:00Z"
}

Track the time of the last heartbeat. If you miss several in a row, treat the connection as dead and reconnect.

new_reading​

A sensor reading was just stored for a machine, whether it came from the Edge Agent or direct ingestion.

{
"type": "new_reading",
"machine_id": "9c8b7a6d-5e4f-3a2b-1c0d-9e8f7a6b5c4d",
"metric_name": "temperature",
"value": 73.2,
"unit": "celsius",
"timestamp": "2026-08-05T10:30:05Z"
}

When an Edge Agent catches up after an outage, its backlog is not streamed reading by reading: of the readings that were already more than 2 minutes old on arrival, each batch sends only the newest for each machine and metric. Readings of tags not yet assigned to a machine are not streamed.

new_alert​

An alert fired: a monitored metric crossed its configured threshold or left its learned normal range.

{
"type": "new_alert",
"alert_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"machine_id": "9c8b7a6d-5e4f-3a2b-1c0d-9e8f7a6b5c4d",
"severity": "critical",
"message": "Temperature exceeded threshold"
}

Handle unknown type values gracefully: ignore messages you don't recognize so new event types don't break your client.

The stream does not carry machine status changes. To show each machine's current status, read GET /api/v1/fleet/status (see Fleet) when the page loads, and again when a new_alert arrives.

Connection lifecycle and close codes​

When the server closes the connection, the close code tells you why and what to do:

CodeMeaningWhat to do
4001Invalid or expired token, or the session was endedRefresh the JWT, then reconnect
4008Authentication timed out (token not sent within 4 seconds)Send the token immediately in onopen
4013Server busyReconnect after a short, randomized delay
4029Too many connections from your address, or your company's connection limit reachedBack off and retry later; reuse a single connection

The socket is also re-checked every 30 seconds while open: if your session ends elsewhere (sign-out, password change, a deactivated account) or your token expires, the server closes the connection with 4001 and you should reconnect with a fresh token. Access tokens last 30 minutes, so refresh before then and reconnect with the new token.

Companies have a cap on concurrent connections, and exceeding it closes new sockets with 4029. Share one connection across your app rather than opening one per component or tab (see Performance tips).

Reconnection with backoff​

Network drops are normal. Reconnect automatically, and back off exponentially so you don't hammer the server. If you were closed with 4001, refresh the token before reconnecting.

class HaltlessSocket {
private ws: WebSocket | null = null;
private attempts = 0;
private readonly maxDelayMs = 30_000;

constructor(private accessToken: string) {
this.connect();
}

private connect() {
this.ws = new WebSocket("wss://api.haltless.io/api/v1/ws/dashboard");

this.ws.onopen = () => {
this.ws!.send(this.accessToken);
this.attempts = 0; // reset backoff on a successful connect
};

this.ws.onmessage = (event) => this.handle(JSON.parse(event.data));

this.ws.onclose = (event) => {
if (event.code === 4001) {
// Token expired: refresh, then reconnect.
this.refreshTokenAndReconnect();
return;
}
this.scheduleReconnect();
};
}

private scheduleReconnect() {
const delay = Math.min(1000 * 2 ** this.attempts, this.maxDelayMs);
this.attempts++;
setTimeout(() => this.connect(), delay);
}

private async refreshTokenAndReconnect() {
// Exchange your refresh token for a new access token, then reconnect.
const res = await fetch("https://api.haltless.io/api/v1/auth/refresh", {
method: "POST",
credentials: "include",
});
const { access_token } = await res.json();
this.accessToken = access_token;
this.connect();
}

private handle(data: any) {
switch (data.type) {
case "new_reading":
// Update your charts.
break;
case "new_alert":
// Surface a notification.
break;
}
}

disconnect() {
this.ws?.close();
}
}

const socket = new HaltlessSocket("YOUR_JWT_ACCESS_TOKEN");

React example​

A small hook that connects, authenticates, and exposes the latest message:

import { useEffect, useRef, useState, useCallback } from "react";

export function useHaltlessSocket(accessToken: string | null) {
const wsRef = useRef<WebSocket | null>(null);
const [lastMessage, setLastMessage] = useState<any>(null);
const [isConnected, setIsConnected] = useState(false);

const connect = useCallback(() => {
if (!accessToken) return;
const ws = new WebSocket("wss://api.haltless.io/api/v1/ws/dashboard");

ws.onopen = () => {
ws.send(accessToken);
setIsConnected(true);
};

ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type !== "heartbeat") setLastMessage(data);
};

ws.onclose = () => {
setIsConnected(false);
setTimeout(connect, 2000); // simple reconnect
};

wsRef.current = ws;
}, [accessToken]);

useEffect(() => {
connect();
return () => wsRef.current?.close();
}, [connect]);

return { lastMessage, isConnected };
}

Performance tips​

  1. Filter client-side. The stream covers every machine your user may see. Filter by machine_id in your handler to show only what a given view needs.
  2. Throttle UI updates. Readings can arrive rapidly. Batch updates with requestAnimationFrame or a throttle to avoid excessive re-renders.
  3. Use one connection. Open a single socket and share it across your app via context or a store. Don't open one per component or tab.
  4. Watch the heartbeat. If the last heartbeat is older than a few intervals, reconnect proactively instead of waiting for a close event.

Next steps​