WebSocket Integration
Overview
Build real-time dashboards and monitoring tools by subscribing to the Haltless WebSocket. Once connected, you receive a live stream of everything happening in your workspace , new sensor readings as they're ingested and alerts as they fire , without polling.
| URL | wss://api.haltless.io/api/v1/ws/dashboard |
| Auth | Send your JWT access token as the first message after connecting |
| Keep-alive | Server sends a heartbeat roughly every 30 seconds |
Client Haltless
│ │
│──── WebSocket connect ────────▶│
│ │
│──── send JWT (first message) ─▶│
│ │
│◀──── heartbeat ────────────────│ (periodic)
│◀──── new_reading ──────────────│
│◀──── new_alert ────────────────│
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 a few 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;
}
};
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 periodically 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 ingested , whether 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"
}
new_alert
An alert fired , a monitored metric crossed its configured threshold or an anomaly was detected.
{
"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.
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 | Refresh the JWT, then reconnect |
4008 | Authentication timeout | Send the token immediately in onopen |
4029 | Connection limit reached | Back off and retry later; reuse a single connection |
The socket is also re-checked periodically while open: if your session ends elsewhere (sign-out, password change, or an expired token), the server closes the connection and you should reconnect with a fresh token.
Each workspace can hold up to 50 concurrent connections. 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 your whole workspace. 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