Skip to content
Documentation menu

Realtime events

ZebChat pushes changes over Socket.IO 4 as they happen: new messages, typing, receipts, assignments and visitors coming and going.

Connecting

Connect to the API origin (path /socket.io) on one of two namespaces:

  • /agent for agent apps. Authenticate with an access token and the organization to follow: { token, organizationId }. You need the conversations:read permission. One socket follows one organization; reconnect to switch.
  • /visitor is used by the widget, with a visitor token.
JavaScript
import { io } from 'socket.io-client';

const socket = io('https://api.zebchat.com/agent', {
  transports: ['websocket'],
  auth: { token: accessToken, organizationId: 'org_…' },
});

socket.on('message.created', (envelope) => {
  console.log(envelope.conversationId, envelope.data.body);
});

// Commands are acknowledged: { ok: true, data } or { ok: false, error: { status, message } }
const ack = await socket.emitWithAck('message.send', {
  conversationId: 'conv_…',
  body: 'Hello!',
  clientMessageId: crypto.randomUUID(),
});

A failed handshake ends in connect_error with the message Unauthorized (refresh the access token once and reconnect) or Forbidden (no access to that organization any more).

The event envelope

Every event the server sends is wrapped in a versioned envelope. New optional fields and new events keep version: 1; a breaking change bumps the version, and both versions are sent during a transition.

TypeScript
interface RealtimeEnvelope<TData> {
  event: string;            // 'message.created', … (also the Socket.IO event name)
  version: 1;
  organizationId: string;   // org_…
  conversationId?: string;  // conv_…, on conversation events
  timestamp: string;        // ISO 8601, UTC
  data: TData;
}

Events

EventSent toWhen
conversation.createdAgentsA new conversation
conversation.updatedAgents, visitorStatus, assignment, tags, priority or queue position changed
conversation.reopened / .closedAgents, visitorA conversation was reopened or closed
conversation.assignedAgentsAssigned or unassigned, with the reason
conversation.transferredAgentsTransferred to an agent or team, with the note
message.createdAgents, visitorA new message (visitors never get internal notes)
message.delivered / .readAgents, visitorDelivery and read receipts
typing.started / .stoppedAgents, visitorSomeone is typing
visitor.connectedAgentsA visitor opened your site
visitor.updatedAgentsAn online visitor moved to another page
visitor.disconnectedAgentsA visitor’s last tab closed
agent.online / .status / .offlineAgentsAgent presence and status
agent.joined / .leftAgentsA co-agent joined or left a chat, or a supervisor started or stopped monitoring it
conversation.ratedAgentsThe visitor rated an ended chat or changed the rating
ai.typing / message.deltaAgents, visitorThe AI assistant is writing: draft chunks to append, then the final message.created
ai.statusAgentsThe AI key failed, the spend cap was reached, or AI resumed
knowledge.source.updated / .deletedAgentsKnowledge-base source progress and changes
availability.updatedVisitors of a websiteThe widget went online or offline (agents online, business hours) or its team faces changed

data has the same shape as the matching REST resource: a Conversation as returned by GET …/conversations/{id}, a message as in GET …/messages, a live visitor as in GET …/visitors/live.

Commands

Clients send commands with an acknowledgement (emitWithAck). Errors use HTTP status codes: 400 invalid payload, 403 not allowed, 404 not found, 409 conversation closed.

CommandPayload
message.send{conversationId, body, clientMessageId, attachmentIds?, internal?}: replies with the stored message
typing{conversationId, typing: true | false}: send at most every few seconds
message.delivered{conversationId, messageId}
message.read{conversationId, messageId}: also resets your unread count
agent.status{status: online | away | busy} (agents only)

Delivery guarantees

  1. Every message gets a seq number, strictly increasing within its conversation. Events are sent only after the change is saved.
  2. After every connect and reconnect, fetch what you missed with GET …/conversations/{id}/messages?after=<last message id>.
  3. De-duplicate by message id, and match your own optimistic messages by their clientMessageId. Retrying a send with the same clientMessageId never creates a second message.
  4. Internal notes also take a seq, so visitors may see gaps in the numbers. That is expected.

Presence (who is online) is not replayed: after connecting, load GET /agents and GET /visitors/live, then apply the agent.* and visitor.* events.

Server-side disconnects

A socket’s permissions are fixed when it connects, so the server closes agent sockets when access changes: on logout, a revoked or expired session, a password change or reset, or a role change or removal. Socket.IO then reports io server disconnect and does not reconnect by itself. Reconnect once with fresh credentials; the handshake decides whether access continues.

Behind a load balancer, use the WebSocket transport (as in the example above) or sticky sessions. See the REST API overview for authentication.