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

FeatureSSEWebSocket (Socket.io)
DirectionServer to client (one-way)Bidirectional
ProtocolHTTP/1.1+WebSocket (ws://)
ReconnectionBuilt-in auto-reconnectSocket.io handles reconnect
Browser supportNative EventSource APIRequires socket.io-client library
Best forDashboards, notification feedsInteractive apps needing acknowledgment
AuthenticationQuery parameter or headerAuth 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/alerts

Connecting 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

EventDescription
alertA new alert has been created
alert_updatedAn existing alert has been updated (acknowledged, snoozed, notes added)
alert_resolvedAn alert has been resolved
heartbeatKeepalive 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-client

JavaScript 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