message.comDevelopers

Authentication and presence Live

The /agent namespace requires a workspace JWT in auth.token. The /widget namespace accepts an anonymous handshake. Both contribute to the presence model: who is online, who is available to take work, and which conversations are being read in real time.

JWT in auth.token

Pass the workspace JWT under auth.token when opening the socket. The token is the same one returned by POST /api/v1/auth/login and used for REST calls. The server verifies the signature, expiry, and tokenVersion match.

JavaScript
import { io } from "socket.io-client";

const socket = io("wss://app.message.com/agent", {
  auth: {
    token: workspaceJwt   // from POST /api/v1/auth/login
  },
  transports: ["websocket"]
});

socket.on("connect_error", (err) => {
  if (err.message === "unauthorized") {
    // JWT expired or revoked; re-login
    refreshAuthAndReconnect();
  }
});

Do not pass the JWT in a query parameter. Query strings end up in proxy logs and browser histories. Socket.io auth is transmitted in the handshake body and is the right place for credentials.

Token lifecycle

  • JWTs expire after 7 days.
  • Logging out of the dashboard (POST /api/v1/auth/logout) bumps tokenVersion, invalidating every live token for that agent.
  • Re-issuing a token mid-session is supported. Set socket.auth = { token: newJwt } and call socket.connect().
JavaScript
async function refreshAuthAndReconnect() {
  const { token } = await fetch("/api/auth/refresh", { method: "POST" }).then(r => r.json());
  socket.auth = { token };
  socket.connect();
}

Connect errors

ErrorCauseAction
unauthorizedMissing, malformed, expired, or version-bumped token.Refresh auth, retry.
workspace_suspendedWorkspace billing or compliance suspension.Surface in-product warning.
too_many_connectionsPer-agent connection cap exceeded (default 5 simultaneous).Close stale sockets.

Presence model

Presence is computed from socket state plus the explicit agent:status flag. The status flag has three values:

  • available, connected, willing to take new work. Counts toward routing eligibility.
  • away, connected, not taking new work (e.g., on a break). Routing skips.
  • offline, explicitly off. Treated the same as no socket connected.
JavaScript
// Agent sets status on connect (default is 'available')
socket.emit("agent:status", { status: "available" });
socket.emit("agent:status", { status: "away" });
socket.emit("agent:status", { status: "offline" });

// Receive status updates for the team
socket.on("agent:online", ({ agentId, status }) => { /* ... */ });
socket.on("agent:offline", ({ agentId }) => { /* ... */ });

If the socket drops without setting offline, the agent stays in their last status for a 60-second grace period (to ride out brief network blips). After the grace window, the server flips them to offline.

Visitor presence

On the widget namespace, presence is socket-driven. A visitor is “active” while at least one of their sessions has an open socket. The agent dashboard receives visitor:online and visitor:offline events tagged with the visitorId.

JavaScript
// Widget side: receive presence of the assigned agent
socket.on("agent:typing", ({ agentName }) => showTypingChip(agentName));
socket.on("agent:typing-stopped", () => clearTypingChip());

Multi-tab and multi-device

One agent can connect from multiple tabs, devices, or apps simultaneously. All sockets join the same agent-scoped rooms. Status updates from any socket overwrite the agent's server-side status. Emits are de-duplicated by message ID to keep multi-tab sending from creating duplicates.

How status feeds routing

Routing rules read agent:status at the moment of dispatch:

  • New chats and tickets land first with agents whose status is available, within the conversation's channel and department scope.
  • Phone routing only rings agents whose status is available and whose channels include calls.
  • If no agent matches, the conversation queues until one becomes available.

For role and department scoping see the RBAC notes in Agent concept and Department vs team.

Missed-event replay

On reconnect, the server replays events the agent missed during a disconnect window of up to 5 minutes. Older gaps require a REST refetch of the affected conversations. The replay is best-effort and tagged with a replayed: true field so client UIs can suppress notification sounds.

Common pitfalls

  • Refreshing token without reconnect. Setting socket.auth alone does not push the new token. Call socket.connect() (or wait for the next reconnect attempt) for it to take effect.
  • Treating away as offline. They are different. Agents in away still receive events for conversations they already own; they just do not get new routing.
  • Trusting browser tab visibility. The status flag is server-controlled. Setting document.hidden in the browser does not change presence; emit agent:status explicitly if you want away-on-tab-hide behaviour.
  • Not handling workspace_suspended. A billing failure manifests as a connect rejection here. Surface a clear in-app warning rather than retrying silently.