Real-time Events
SALLY provides two mechanisms for receiving alerts in real time: Server-Sent Events (SSE) and WebSocket via Socket.io. Both deliver alerts the moment they are generated, eliminating the need to poll the REST API.
Choosing Between SSE and WebSocket
| Feature | SSE | WebSocket (Socket.io) |
|---|---|---|
| Direction | Server to client (one-way) | Bidirectional |
| Protocol | HTTP/1.1+ | WebSocket (ws://) |
| Reconnection | Built-in auto-reconnect | Socket.io handles reconnect |
| Browser support | Native EventSource API | Requires socket.io-client library |
| Best for | Dashboards, notification feeds | Interactive apps needing acknowledgment |
| Authentication | Query parameter or header | Auth payload on connect |
For most dashboard and notification use cases, SSE is the simpler choice. Use WebSocket when you need bidirectional communication (e.g., acknowledging alerts from the same connection).
Server-Sent Events (SSE)
Endpoint
GET /api/v1/sse/alertsConnecting with curl
curl -N "https://sally-api.apps.appshore.in/api/v1/sse/alerts" \
-H "X-API-Key: $SALLY_API_KEY" \
-H "Accept: text/event-stream"The -N flag disables output buffering so events are displayed as they arrive.
JavaScript (EventSource API)
The browser-native EventSource API is the simplest way to consume SSE. Since EventSource does not support custom headers, pass your API key as a query parameter:
const apiKey = process.env.SALLY_API_KEY;
const url = `https://sally-api.apps.appshore.in/api/v1/sse/alerts?apiKey=${apiKey}`;
const eventSource = new EventSource(url);
eventSource.onopen = () => {
console.log("Connected to alert stream");
};
eventSource.onmessage = (event) => {
const alert = JSON.parse(event.data);
console.log("New alert:", alert.type, alert.priority);
handleAlert(alert);
};
eventSource.onerror = (error) => {
console.error("SSE connection error:", error);
// EventSource automatically reconnects
};
function handleAlert(alert) {
switch (alert.priority) {
case "CRITICAL":
showUrgentNotification(alert);
playAlertSound();
break;
case "HIGH":
showNotification(alert);
break;
case "MEDIUM":
updateAlertFeed(alert);
break;
case "LOW":
addToAlertLog(alert);
break;
}
}Using fetch for SSE (Node.js)
In Node.js environments where EventSource is not available natively, use a streaming fetch approach:
async function subscribeToAlerts(apiKey) {
const response = await fetch(
"https://sally-api.apps.appshore.in/api/v1/sse/alerts",
{
headers: {
"X-API-Key": apiKey,
Accept: "text/event-stream",
},
}
);
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop(); // Keep incomplete line in buffer
for (const line of lines) {
if (line.startsWith("data: ")) {
const data = line.slice(6);
if (data.trim()) {
const alert = JSON.parse(data);
console.log("Alert received:", alert.type);
}
}
}
}
}
subscribeToAlerts(process.env.SALLY_API_KEY);SSE Event Format
Each event follows the standard SSE format:
event: alert
data: {"id":"alt_x1y2z3w4","type":"HOS_APPROACHING_DRIVE_LIMIT","category":"HOS_VIOLATION","priority":"HIGH","status":"ACTIVE","title":"Driver approaching 11-hour drive limit","message":"Mike Johnson (TRK-4821) has 45 minutes of drive time remaining.","driverId":"drv_a1b2c3d4","routePlanId":"rte_f8e7d6c5","createdAt":"2026-02-10T16:15:00Z"}
event: alert_updated
data: {"id":"alt_x1y2z3w4","status":"ACKNOWLEDGED","acknowledgedBy":"usr_8a2b3c4d","acknowledgedAt":"2026-02-10T16:18:00Z"}
event: alert_resolved
data: {"id":"alt_x1y2z3w4","status":"RESOLVED","resolvedBy":"usr_8a2b3c4d","resolvedAt":"2026-02-10T16:45:00Z","resolution":"Driver took required rest stop at Pilot Travel Center, Lebanon IN."}
event: heartbeat
data: {"timestamp":"2026-02-10T16:20:00Z"}
SSE Event Types
| Event | Description |
|---|---|
alert | A new alert has been created |
alert_updated | An existing alert has been updated (acknowledged, snoozed, notes added) |
alert_resolved | An alert has been resolved |
heartbeat | Keepalive signal sent every 30 seconds |
The heartbeat event helps you detect stale connections. If you do not receive a heartbeat within 60 seconds, the connection may have dropped and should be re-established.
WebSocket (Socket.io)
Connecting
Install the Socket.io client:
pnpm install socket.io-clientJavaScript Client Setup
import { io } from "socket.io-client";
const socket = io("https://sally-api.apps.appshore.in", {
path: "/socket.io",
auth: {
token: process.env.SALLY_API_KEY, // or JWT token
},
transports: ["websocket"],
});
socket.on("connect", () => {
console.log("Connected to SALLY WebSocket");
console.log("Socket ID:", socket.id);
});
socket.on("disconnect", (reason) => {
console.log("Disconnected:", reason);
// Socket.io automatically reconnects
});
socket.on("connect_error", (error) => {
console.error("Connection error:", error.message);
});Subscribing to Alert Events
// New alert created
socket.on("alert:new", (alert) => {
console.log("New alert:", alert.type, alert.priority);
handleNewAlert(alert);
});
// Alert status changed
socket.on("alert:updated", (update) => {
console.log("Alert updated:", update.id, update.status);
updateAlertInUI(update);
});
// Alert resolved
socket.on("alert:resolved", (resolution) => {
console.log("Alert resolved:", resolution.id);
removeFromActiveAlerts(resolution);
});
// Route status changed
socket.on("route:updated", (routeUpdate) => {
console.log("Route update:", routeUpdate.planId, routeUpdate.status);
updateRouteInUI(routeUpdate);
});Acknowledging Alerts via WebSocket
With WebSocket, you can acknowledge alerts through the same connection:
// Acknowledge an alert
socket.emit("alert:acknowledge", {
alertId: "alt_x1y2z3w4",
message: "Contacting driver now",
});
// Listen for acknowledgment confirmation
socket.on("alert:acknowledge:success", (result) => {
console.log("Alert acknowledged:", result.alertId);
});
socket.on("alert:acknowledge:error", (error) => {
console.error("Failed to acknowledge:", error.message);
});Full WebSocket Example
import { io } from "socket.io-client";
const API_KEY = process.env.SALLY_API_KEY;
const socket = io("https://sally-api.apps.appshore.in", {
auth: { token: API_KEY },
transports: ["websocket"],
reconnection: true,
reconnectionAttempts: 10,
reconnectionDelay: 1000,
reconnectionDelayMax: 30000,
});
// Connection lifecycle
socket.on("connect", () => console.log("Connected"));
socket.on("disconnect", (reason) => console.log("Disconnected:", reason));
socket.on("reconnect_attempt", (n) => console.log("Reconnect attempt:", n));
// Alert handlers
socket.on("alert:new", (alert) => {
console.log(`[${alert.priority}] ${alert.title}`);
if (alert.priority === "CRITICAL") {
// Auto-acknowledge critical alerts and notify on-call dispatcher
socket.emit("alert:acknowledge", {
alertId: alert.id,
message: "Auto-acknowledged. Paging on-call dispatcher.",
});
notifyOnCallDispatcher(alert);
}
});
socket.on("alert:updated", (update) => {
console.log(`Alert ${update.id} -> ${update.status}`);
});
socket.on("alert:resolved", (resolution) => {
console.log(`Alert ${resolution.id} resolved`);
});
// Graceful shutdown
process.on("SIGINT", () => {
console.log("Disconnecting...");
socket.disconnect();
process.exit(0);
});Event Payload Schema
All alert events (SSE and WebSocket) share the same payload structure:
{
"id": "alt_x1y2z3w4",
"type": "HOS_APPROACHING_DRIVE_LIMIT",
"category": "HOS_VIOLATION",
"priority": "HIGH",
"status": "ACTIVE",
"title": "Driver approaching 11-hour drive limit",
"message": "Mike Johnson (TRK-4821) has 45 minutes of drive time remaining.",
"driverId": "drv_a1b2c3d4",
"driverName": "Mike Johnson",
"vehicleId": "veh_e5f6g7h8",
"vehicleUnit": "TRK-4821",
"routePlanId": "rte_f8e7d6c5",
"metadata": {
"currentHoursDriven": 10.25,
"driveTimeRemaining": 0.75,
"recommendedAction": "PLAN_REST_STOP"
},
"createdAt": "2026-02-10T16:15:00Z"
}Connection Best Practices
Implement Reconnection Logic
Both SSE and Socket.io handle reconnection automatically, but you should:
- Log reconnection events for debugging
- Re-fetch missed alerts after reconnection by calling
GET /api/v1/alerts?status=ACTIVE - Display connection status to the user in the dashboard
Handle Duplicate Events
In rare cases during reconnection, you may receive the same event twice. Use the alert id to deduplicate:
const processedAlerts = new Set();
function handleAlert(alert) {
if (processedAlerts.has(alert.id)) return;
processedAlerts.add(alert.id);
// Process the alert...
// Clean up old entries periodically
if (processedAlerts.size > 1000) {
const entries = [...processedAlerts];
entries.slice(0, 500).forEach((id) => processedAlerts.delete(id));
}
}Respect Connection Limits
Each tenant is limited to 10 concurrent SSE connections and 10 concurrent WebSocket connections. If you exceed this limit, the oldest connection is closed.
Next Steps
- Alert Types — Reference for all alert categories
- Alert Management — REST API for managing alerts
- Alerts Overview — System architecture and lifecycle