Widget namespace Live
The /widget namespace is what the embed bundle subscribes to. Anonymous handshake, identified by workspaceId plus a locally-cached visitorId. The widget bundle wires this for you. The reference here covers each event in case you build a custom client.
For the dashboard side see Agent namespace. For connection mechanics see Socket.io overview.
Handshake
Connect anonymously with the public workspace ID. If you have a cached visitor ID, pass it; otherwise the server issues one on the first visitor:connect ack. Persist that ID in local storage so the visitor reattaches to their conversation history on the next visit.
// The widget bundle does this for you. Shown for reference.
import { io } from "socket.io-client";
const socket = io("wss://app.message.com/widget", {
auth: {
workspaceId: "abc123",
visitorId: localStorage.getItem("msg_visitor") || undefined
},
transports: ["websocket"]
});
socket.on("connect", () => {
socket.emit("visitor:connect", {
pageUrl: location.href,
referrer: document.referrer,
locale: navigator.language
}, (err, ack) => {
if (ack?.visitorId) {
localStorage.setItem("msg_visitor", ack.visitorId);
}
});
});Events received by the embed
| Event | Payload | Fires when |
|---|---|---|
message:received | Message with agent details | Agent sent a reply. |
agent:typing | { conversationId, agentId, agentName } | Agent typing indicator starts. |
agent:typing-stopped | { conversationId, agentId } | Agent typing indicator clears. |
conversation:assigned | Conversation with assignedAgent | Agent picked up the chat; the widget shows their name and avatar. |
conversation:closed | Conversation | The chat was closed by the agent. The widget surfaces the post-chat survey. |
visitor:identified | Visitor record | Server confirmed an identify call. |
Events emitted by the embed
| Event | Payload | Effect |
|---|---|---|
visitor:connect | { pageUrl, referrer, locale } | Initial handshake. Server issues a visitorId if none provided. |
visitor:identify | Visitor identification payload | Links session to a customer record. HMAC-signed in production. |
message:send | { body, attachments? } | Visitor sends a message. Creates a conversation on the first message. |
message:typing | None | Broadcast typing indicator to the assigned agent. |
message:typing-stopped | None | Clear the typing indicator. |
visitor:identify
Promotes an anonymous visitor to an identified record. In production, the payload must include a userHash signature; without it the server records the identification but flags the record as unverified.
socket.emit("visitor:identify", {
email: "[email protected]",
userId: "crm-4567",
userHash: "<server-side HMAC>",
name: "Alice Johnson",
plan: "pro"
});For HMAC computation, see Identifying visitors.
message:send
Sends a visitor message. The first message:send in a session creates a new conversation; subsequent messages append to the latest open conversation for that visitor.
socket.emit("message:send", {
body: "Hi, I have a question about my order #4567"
}, (err, msg) => {
if (err) console.error(err);
// msg is the persisted visitor message
});Errors: rate_limited for burst (3 messages per second), workspace_offline if the workspace has been suspended, visitor_blocked if the visitor is on the abuse list.
message:typing
Debounce and emit while the visitor is typing. Stop after 1.5 seconds of inactivity. The widget bundle handles this; if you build a custom client, the pattern is:
let typingTimer;
input.addEventListener("input", () => {
socket.emit("message:typing");
clearTimeout(typingTimer);
typingTimer = setTimeout(() => {
socket.emit("message:typing-stopped");
}, 1500);
});Reconnection and offline behaviour
If the socket disconnects mid-conversation, the widget shows a quiet “reconnecting” chip in the header. Messages composed during a disconnect are queued locally and flushed on reconnect. The server replays any agent messages the visitor missed (up to the 5-minute replay window).
Common pitfalls
- Not persisting the
visitorId. If you do not write the ID to local storage after the first connect ack, every page reload starts a new visitor record. Your inbox fills with duplicates. - Identifying without HMAC. Without
userHash, anyone with dev tools can pretend to be anyone. Always sign in production. - Skipping the ack on
message:send. Without the ack you cannot tell if the message was rate-limited or rejected. - Spamming typing events. Debounce them. Sending one per keystroke wastes bandwidth and creates flicker on the agent side.