message.comDevelopers

Socket.io overview Live

Real-time fan-out runs over Socket.io with two namespaces. The dashboard subscribes to /agent with a workspace JWT. The widget bundle subscribes to /widget anonymously, identified by workspace plus a local visitor ID. Both deliver sub-100ms latency in our internal benchmarks.

Socket.io is a thin wrapper over WebSockets with reconnect logic, fallback transports, and packet acks. Read the Socket.io v4 docs for transport details. We pin Socket.io v4 server-side and ship Socket.io v4 in the widget bundle.

Base URL

The same hostname serves the REST API and the Socket.io upgrade:

WebSocket URL
wss://app.message.com/socket.io/

The path is fixed at /socket.io/ (we do not allow custom paths). The namespace is appended via the io(URL) call.

Namespaces

NamespaceWho connectsAuth
/agentDashboard, human agents, server-side automations acting as a workspaceJWT in auth.token
/widgetEmbed bundle, customer browsersAnonymous handshake with workspaceId + optional visitorId

Detailed event lists per namespace: Agent namespace and Widget namespace.

Agent connection

Get a workspace JWT from POST /api/v1/auth/login (see Authentication). Pass it under auth.token on connect. The server verifies the JWT, attaches the agent to the workspace room, and replays any events the agent missed while disconnected (up to a 5-minute window).

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

const socket = io("wss://app.message.com/agent", {
  auth: { token: "<workspace JWT>" },
  transports: ["websocket"],          // skip the long-poll upgrade
  reconnectionDelayMax: 5000
});

socket.on("connect", () => {
  console.log("connected as", socket.id);
});

socket.on("conversation:new", (conv) => {
  // new chat just landed in your queue
});

Widget connection

The widget bundle handles this automatically. The handshake passes the public workspace ID plus a locally-cached visitor ID (if any). The server may issue a new visitor ID and persist it in local storage.

JavaScript (reference)
// The widget bundle does this for you. Shown for reference.
const socket = io("wss://app.message.com/widget", {
  auth: {
    workspaceId: "abc123",
    visitorId: "v_local_uuid"
  },
  transports: ["websocket"]
});

Presence

Each connected socket counts toward presence. Agents go “online” when they connect with an available status flag; they go “away” on disconnect. Visitors are “active” while their socket is open. See Authentication and presence for the full state machine.

Reconnection

The Socket.io client reconnects automatically with exponential backoff. Most failures (network blip, server restart) self-heal. A few cases need explicit handling:

JavaScript
socket.on("disconnect", (reason) => {
  // 'io server disconnect' = server kicked us, manual reconnect needed
  if (reason === "io server disconnect") {
    socket.connect();
  }
  // any other reason: auto-reconnect handles it
});

socket.io.on("reconnect_attempt", (n) => {
  console.log("reconnect attempt", n);
});

If the JWT has expired between connect attempts, the server rejects the handshake. Re-issue the token from your auth flow and reconnect.

Event payload shapes

All events carry JSON payloads. Resource references use UUIDs (string). Timestamps are ISO 8601 strings with a Z suffix (UTC). Money amounts are integer cents (no floats).

Per-event payload shapes are documented on the namespace pages. Most match the REST resource shape one-for-one. For example, the conversation:new payload is identical to the conversation object returned by GET /api/v1/conversations.

Acknowledgements

Client-emitted events can optionally request a server ack. The ack callback fires with either the resource or an error. Use this for take-next (atomic claim of the next chat), message:send, and other state-changing emits.

JavaScript
socket.emit("take-next", {}, (err, conv) => {
  if (err) {
    if (err === "already_taken") return;  // another agent got it
    if (err === "queue_empty") return;
    throw new Error(err);
  }
  // conv is the conversation you just claimed
});

Common pitfalls

  • Hard-coding https. Use wss in the URL. Socket.io will negotiate the upgrade, but pinning https blocks the WebSocket transport on some proxies.
  • Polling for events. Do not also poll the REST API for new messages while subscribed. You will get duplicates and waste rate limit. Pick one source.
  • Skipping the ack on take-next. Without the ack, you cannot tell whether you actually got the conversation. Two agents emitting at the same time is the whole reason the ack exists.
  • Listening for every event. Subscribe only to events your UI actually renders. Idle subscribers still cost a function pointer per packet.