Documentation

WebSocket API

Connect, authenticate, send messages, and listen for streams on the /chat Socket.IO namespace.

Live chat goes over Socket.IO at namespace /chat. Customers, agents, and read-only broadcast viewers all share this namespace — server-side handlers gate per role.

Connection

Connect with { token, orgId } in the handshake auth. The token is a JWT issued by POST /profile/signin (agent/user) or POST /profile/customer/signin (customer).

import { io } from 'socket.io-client';

const socket = io('https://appengine.appmint.io/chat', {
  transports: ['websocket'],
  auth: {
    token: '<jwt>',
    orgId: '<your-org-id>',
    deviceId: 'device-stable-uuid',
    context: {
      chatSessionId: localStorage.getItem('chatSessionId') || undefined,
      configId: '<chat-config-id>',
      currentUrl: window.location.href,
    },
  },
});

socket.on('authenticate', (result) => {
  if (!result.success) return console.error(result.error);
  if (result.chatSessionId) {
    localStorage.setItem('chatSessionId', result.chatSessionId);
  }
});

The server emits authenticate once it has verified the token. Failure causes an immediate disconnect.

Authentication response

{
  success: true,
  user: { email: '[email protected]', name: 'Alice', role: 'customer' },
  chatSessionId: 'cs_abc...',  // customer only
  isGuest: false,              // customer only
  config: {                    // when configId was passed
    configId: '...',
    ai: '<assistantId>',
    agents: [{ user: '[email protected]' }],
    status: 'active',
    defaultPath: '/'
  }
}

For broadcast-only viewers (audio/video receive), pass { broadcastOnly: true, orgId, deviceId } in the handshake. No token required, no message permissions granted.

Events the client emits

chat-message — primary send path

socket.emit('chat-message', {
  message: 'Hi, I need help with my order',
  chatSessionId: '<session>',  // customer
  files: [],                   // optional uploads
  assistantId: '<override>',   // optional, defaults to session.assistantId
});

Behaviour depends on session state:

  • AI path — config has an ai assistant and no human is assigned. The server broadcasts the user message to the chat room, then streams AI tokens via chat-stream.
  • Agent path — a human agent is assigned (via pick-next or takeover-chat). The message is delivered as message to the agent's identity room.
  • Queue path — no AI, no agent. The customer is enqueued and the agent panels receive queue-notification.

Other client events

EventRolePurpose
authenticateanyRe-authenticate without disconnecting
update-contextanyUpdate currentUrl, location, device, etc.
set-statusagent'available' | 'busy' | 'away'
list-online-agentsagentList agents, optionally filter by skill
pick-nextagentPop the next queued chat
transfer-chatagentHand the chat to another agent
takeover-chatagentTake an AI conversation
resume-aiagentHand it back to the AI
close-chatagentEnd the chat
select-assistantcustomerChoose an AI assistant before sending
list-assistantsanyAvailable assistants for this org
list-chatsagentChat list for the admin panel
chat-historyanyReplay messages for a chatId
sendMessageanyPeer-to-peer send (no AI) — staff DM, etc.
join-chat / leave-chatagentObserve a room without claiming it
archive-chat / unarchive-chatagentLifecycle
broadcast-start / broadcast-endagentStart a WebRTC broadcast to widgets
engage-visitoragentProactive chat invitation to a visitor
webrtc-offer / webrtc-answer / ice-candidateanyWebRTC signaling for broadcast

Events the server emits

chat-stream — AI streaming

socket.on('chat-stream', (evt) => {
  switch (evt.event) {
    case 'chunk':       appendText(evt.data); break;
    case 'tool-use':    showTool(evt.tool, evt.input); break;
    case 'tool-result': showResult(evt.tool, evt.result); break;
    case 'end':         finalize(evt.sk); break;
    case 'error':       showError(evt.error); break;
  }
});

Other server events

EventWhen
messageA chat message arrived (system, user, ai-assistant, or peer)
presence-changeA user came online / went offline / changed status
agent-assignedAn agent picked up the customer's chat
chat-endedChat closed (by anyone, or system inactivity)
queued / queue-updateCustomer's position update (every 15s while waiting)
queue-notificationAgent panels — a customer is waiting
queue-updatedQueue mutated (added, picked, removed)
broadcast-started / broadcast-endedWebRTC broadcast lifecycle

Message shape

The server sends both flat and nested fields so widget and admin clients both work:

{
  messageId: 'sk-abc',
  sk: 'sk-abc',
  chatId: 'customer:[email protected]',
  from: '[email protected]',
  to: '[email protected]',
  content: 'Hello',
  type: 'user' | 'ai-assistant' | 'system',
  senderRole: 'customer' | 'agent',
  sentTime: 1714000000000,
  status: 'sent',
  files: [...],
  data: { /* same fields nested */ },
}

Inactivity timeouts

Once an agent picks up a customer, two Redis keys arm:

  • chat-response-timeout:{orgId}:{chatId} — 5 min, expects a customer reply
  • chat-global-timeout:{orgId}:{chatId} — 30 min hard cap

Warnings fire 1 minute before each timeout. On expiry the server emits a system message, then chat-ended with reason: 'inactivity'. Any new customer message resets both keys.

The widget should send chatSessionId from localStorage on every connect. Without it, every reconnect creates a brand new session and the user's history disappears.