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.
| URL | wss://api.haltless.io/api/v1/ws/dashboard |
| Auth | Send your JWT access token as the first message, within 4 seconds of connecting |
| Keep-alive | Server sends a heartbeat every 30 seconds |
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:
| Code | Meaning | What to do |
|---|---|---|
4001 | Invalid or expired token, or the session was ended | Refresh the JWT, then reconnect |
4008 | Authentication timed out (token not sent within 4 seconds) | Send the token immediately in onopen |
4013 | Server busy | Reconnect after a short, randomized delay |
4029 | Too many connections from your address, or your company's connection limit reached | Back 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
- Filter client-side. The stream covers every machine your user may see. Filter by
machine_idin your handler to show only what a given view needs. - Throttle UI updates. Readings can arrive rapidly. Batch updates with
requestAnimationFrameor a throttle to avoid excessive re-renders. - 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.
- Watch the heartbeat. If the last heartbeat is older than a few intervals, reconnect proactively instead of waiting for a close event.
Next steps
- WebSocket API Reference: message schemas and connection details
- Sensor Data API: query historical readings over REST
- Alerts API: fetch and reconcile alert history
- Direct Ingestion: push the readings that produce these events