Chat Bots

This page explains how the web channel chat flow works in the Dialogs chatbot engine (channel-web REST module + guest Socket.IO connection), and how to build a custom chat frontend against it. No prior knowledge of Dialogs internals is required — every term is defined before it's used. Scope: web channel only — text/web chat, agent handoff (human takeover), and click-to-call (voice/video call from the chat UI). Standalone voice, WhatsApp, and other non-web channels are covered on their own pages.

Pick your platform — once you understand the flow below, jump straight to the SDK or UI Kit for your platform.

Chatbot SDK — build your own UI

Chatbot UI Kit — pre-built UI

Getting Started

Prerequisites and Configuration

Before you start, you need:

WhatDescriptionWhere to get it
Backend base URLThe root URL of your Dialogs/chatbot-engine server (e.g. https://chat.example.com).From your deployment or backend team.
botIdThe unique identifier of the bot users will chat with (e.g. welcome-bot, support-bot).From bot configuration or your backend team.
ROOT_PATHOptional path prefix under which the API and Socket.IO are mounted (e.g. /api or empty). If your API is at https://host/api/v1/bots/..., ROOT_PATH is /api.From server configuration. Often /api or empty.

If you don't know ROOT_PATH, try /api or ask the team that runs the Dialogs server. If your frontend is on a different origin than the backend, the server must allow your frontend origin in CORS — see CORS and Same-Origin.

Libraries: an HTTP client (fetch or axios) for REST calls, and a Socket.IO client (e.g. socket.io-client) for receiving bot messages in real time.

Flow in one sentence

Your frontend connects to a Socket.IO "guest" connection, gets a webSessionId from it, then uses that webSessionId in every REST request so the backend knows which user is talking. Bot replies and typing come back over the same Socket.IO connection as events.

Key Terms

TermMeaning
visitorIdA stable ID you generate and store (e.g. in localStorage) for the browser/device. Used to reconnect the same "visitor" across page reloads. Should be at least 24 characters (e.g. nanoid).
webSessionIdThe Socket.IO socket id (socket.id) after connecting to the /guest namespace. Required in the body of every channel-web REST request. Valid only while the socket is connected.
conversationIdA UUID identifying one chat thread. You get it from POST /conversations/new (as convoId) or POST /conversations/list (each conversation has an id).
botIdThe bot's unique id; used in the API path and in the socket query.
channel-webThe Dialogs module that serves the web chat REST API and works with the guest socket for realtime delivery.
guest socketThe Socket.IO connection to the /guest namespace. Users (visitors) connect here; the server sends bot messages and typing to this connection.

Architecture Overview

  • Backend: chatbot-engine exposes, for the web channel:
    • REST API: the channel-web module at {baseUrl}{ROOT_PATH}/api/v1/bots/{botId}/mod/channel-web
    • Realtime: Socket.IO namespace /guest — connect to {baseUrl}/guest with path: {ROOT_PATH}/socket.io
  • Auth model: the frontend is identified by a guest Socket.IO connection. Every REST call must send webSessionId (the guest socket id). The backend resolves webSessionIdvisitorIduserId and uses that for conversations/messages.
  • chatbot-dependencies note: the messaging/packages/webchat and messaging/packages/socket modules use a different architecture (MessagingSocket + separate messaging server) for a standalone messaging product. To plug into the existing Dialogs backend, use the channel-web REST + guest socket flow described on this page.

Session and Identity Flow

No cookies, no login

The backend does not use cookies or a separate login to identify the user. Identity is based on the guest Socket.IO connection. You must connect the socket first and use the webSessionId it gives you in every subsequent REST call. Calling any channel-web endpoint (except GET /botInfo) without a valid webSessionId fails (e.g. 401/403 or "session id is invalid").

Guest Socket (Required First)

Order of operations:

  1. Generate a visitorId (e.g. nanoid/uuid, ≥24 chars recommended). Persist it (e.g. localStorage) so the same user gets the same visitorId across reloads.
  2. Connect to Socket.IO guest namespace with query params: visitorId (string), botId (string), handoffId (optional — when opening chat in a handoff context), role (optional — e.g. 'agent' / 'supervisor' for an agent UI).
  3. Wait for the socket to fire connect. Only then read socket.id — this is your webSessionId. If the socket disconnects, reconnect and use the new socket.id for subsequent requests.

Connection URL:

{socketBaseUrl}/guest?visitorId={visitorId}&botId={botId}[&handoffId=<id>][&role=]

Socket path: {ROOT_PATH}/socket.io (e.g. /api/socket.io). Transports: typically ['websocket', 'polling'].

const visitorId = localStorage.getItem('webchat_visitor_id') || nanoid(24);
localStorage.setItem('webchat_visitor_id', visitorId);

const socket = io(`${origin}/guest`, {
  path: `${ROOT_PATH}/socket.io`,
  query: { visitorId, botId: 'your-bot-id' },
  transports: ['websocket', 'polling']
});

let webSessionId = null;
socket.on('connect', () => {
  webSessionId = socket.id;
  // Now you can call channel-web APIs with body.webSessionId = webSessionId
});

// Optional: if the socket disconnects, disable REST calls until reconnected
socket.on('disconnect', (reason) => {
  webSessionId = null;
});

Base Payload for REST

Include in the request body (JSON) for all channel-web endpoints that use assertUserInfo:

FieldTypeRequiredDescription
webSessionIdstringYesGuest socket id (socket.id)

Optional for specific routes: conversationId, payload, etc. — see each endpoint below.

REST API

Base URL and Headers

  • Base URL: {origin}{ROOT_PATH}/api/v1/bots/{botId}/mod/channel-web — e.g. https://your-host.com/api/v1/bots/welcome-bot/mod/channel-web
  • Content-Type: application/json for all POST requests with a JSON body (every endpoint except file upload).
  • Optional auth: for debugger / external auth, send X-BP-ExternalAuth: Bearer {token} or ExternalAuth: Bearer {token}.

All POST bodies below assume you include webSessionId in the JSON body wherever the route requires user identity (all except GET /botInfo).

Endpoints

GET /botInfo

Auth: none (no webSessionId).

Example: GET https://your-host.com/api/v1/bots/welcome-bot/mod/channel-web/botInfo

Response (full example):

{
  "showBotInfoPage": true,
  "name": "Support Bot",
  "description": "Get help 24/7",
  "details": {
    "coverPictureUrl": "https://cdn.example.com/bot-cover.jpg",
    "phoneNumber": "+1234567890",
    "website": "https://example.com",
    "emailAddress": "[email protected]",
    "termsConditions": "https://example.com/terms",
    "privacyPolicy": "https://example.com/privacy",
    "avatarUrl": "https://cdn.example.com/bot-avatar.png",
    "widgetIconUrl": "https://cdn.example.com/widget-icon.png"
  },
  "startup_message": "Hi! How can I help you today?",
  "selfTriggeredBot": false,
  "embeddedMediaIframe": null,
  "clickToCall": null,
  "agentHuntingStatusMessage": null,
  "chatTheme": {
    "widget": { "variant": "soft", "radius": 16, "position": "right", "bubble": "#0066CC", "bottomMargin": 4 }
  },
  "startupForm": null,
  "languages": ["en", "fr"],
  "extraStylesheet": null,
  "disableNotificationSound": false,
  "security": {},
  "lazySocket": false,
  "maxMessageLength": 1000,
  "alwaysScrollDownOnMessages": true
}

Use this to get the bot name, theme, startup form, and supported languages for your UI. clickToCall is 'video', 'voice', or null — see Click-to-Call.

POST /messages — Send User Message

FieldTypeRequiredDescription
webSessionIdstringYesGuest socket id
conversationIdstring (uuid)NoOmit to use or create the most recent conversation
isEmulatorbooleanNoDefault false
payloadobjectYesMust include type and type-specific fields

Example (text message):

{
  "webSessionId": "k9Xj2mNpQr5sTv8wYz1AbC",
  "conversationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "isEmulator": false,
  "payload": { "type": "text", "text": "I need help with my order" }
}

Example (quick reply / postback):

{
  "webSessionId": "k9Xj2mNpQr5sTv8wYz1AbC",
  "payload": { "type": "postback", "text": "Option A", "payload": "OPTION_A" }
}

Supported payload.type (web channel): text, quick_reply, form, login_prompt, visit, request_start_conversation, postback. See Message Payload Types for full examples of each.

Response: 200 (no body). Bot replies and typing are delivered via Socket.IO — see Realtime.

POST /messages/files — Upload File

Request: multipart/form-data:

FieldTypeRequiredDescription
fileFileYesThe file to upload (image, PDF, etc.)
webSessionIdstringYesGuest socket id
conversationIdstring (uuid)YesConversation to attach the file to
payloadstringNoOptional JSON string, e.g. "{}" or "{\"caption\":\"My file\"}"
const formData = new FormData();
formData.append('file', fileBlob, 'document.pdf');
formData.append('webSessionId', 'k9Xj2mNpQr5sTv8wYz1AbC');
formData.append('conversationId', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890');
formData.append('payload', '{}');
fetch(`${baseUrl}/api/v1/bots/welcome-bot/mod/channel-web/messages/files`, {
  method: 'POST',
  body: formData
});

Response: 200 (no body). The file appears as a message (payload type file with url, title, etc.).

POST /events — Send Event

Example (user opened chat — visit):

{
  "webSessionId": "k9Xj2mNpQr5sTv8wYz1AbC",
  "conversationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "selfTriggeredBot": false,
  "payload": { "type": "visit" }
}

Used for visit (user opened chat). The backend may auto-start a conversation or send a welcome message when type === 'visit'. You may omit conversationId to use or create the most recent conversation. Response: 200.

POST /conversations/list — List Conversations

{
  "webSessionId": "k9Xj2mNpQr5sTv8wYz1AbC",
  "startupData": { "name": "Jane Doe", "email": "[email protected]", "phone": "+15551234567", "custom_id": "ext-user-456", "isEmulator": false }
}

Response (full structure with sample data):

{
  "conversations": [
    {
      "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "userId": "u_8k2Lm4nP6qR9sT1vWx3Yz",
      "createdOn": "2025-02-19T10:30:00.000Z",
      "lastMessage": {
        "id": "msg_7f8e9d0c-1b2a-3456-7890-abcdef123456",
        "conversationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
        "authorId": null,
        "sentOn": "2025-02-19T10:35:22.000Z",
        "payload": { "type": "text", "text": "Is there anything else I can help you with?" }
      }
    },
    {
      "id": "b2c3d4e5-f6a7-8901-bcde-f23456789012",
      "userId": "u_8k2Lm4nP6qR9sT1vWx3Yz",
      "createdOn": "2025-02-18T14:00:00.000Z",
      "lastMessage": {
        "id": "msg_8a9b0c1d-2e3f-4567-8901-bcdef2345678",
        "conversationId": "b2c3d4e5-f6a7-8901-bcde-f23456789012",
        "authorId": "u_8k2Lm4nP6qR9sT1vWx3Yz",
        "sentOn": "2025-02-18T14:05:11.000Z",
        "payload": { "type": "text", "text": "Thanks, that fixed it!" }
      }
    }
  ],
  "startNewConvoOnTimeout": true,
  "recentConversationLifetime": "24h"
}

lastMessage.authorId present = last message was from the user; null = from bot/agent. lastMessage may be absent if the conversation has no messages.

POST /conversations/get — Get One Conversation with Messages

{
  "webSessionId": "k9Xj2mNpQr5sTv8wYz1AbC",
  "conversationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}

Response (full structure with sample messages):

{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "userId": "u_8k2Lm4nP6qR9sT1vWx3Yz",
  "createdOn": "2025-02-19T10:30:00.000Z",
  "messages": [
    { "id": "msg_001", "conversationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "authorId": null, "sentOn": "2025-02-19T10:30:01.000Z", "payload": { "type": "text", "text": "Hi! How can I help you today?" } },
    { "id": "msg_002", "conversationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "authorId": "u_8k2Lm4nP6qR9sT1vWx3Yz", "sentOn": "2025-02-19T10:30:15.000Z", "payload": { "type": "text", "text": "I need help with my order" } },
    { "id": "msg_003", "conversationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "authorId": null, "sentOn": "2025-02-19T10:30:16.000Z", "payload": { "type": "text", "text": "Sure! Could you share your order number?" } },
    { "id": "msg_004", "conversationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "authorId": null, "sentOn": "2025-02-19T10:30:17.000Z", "payload": { "type": "quick_reply", "text": "Choose an option:", "choices": [ { "title": "Track order", "value": "track" }, { "title": "Cancel order", "value": "cancel" } ] } }
  ]
}

Message: authorId set = user message; null/absent = bot or agent message. Payload types in history can include text, quick_reply, form, image, file, card, carousel, etc. — e.g. image: { "type": "image", "image": "https://cdn.example.com/photo.jpg" }; file: { "type": "file", "url": "https://cdn.example.com/doc.pdf", "title": "Document.pdf" }. Messages with payload.type === 'visit' or empty text may be filtered by your frontend.

POST /conversations/new — Create New Conversation

{
  "webSessionId": "k9Xj2mNpQr5sTv8wYz1AbC"
}

Response:

{
  "convoId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}

POST /conversations/startupData — Submit Startup Form

{
  "webSessionId": "k9Xj2mNpQr5sTv8wYz1AbC",
  "name": "Jane Doe",
  "phone": "+15551234567",
  "email": "[email protected]",
  "conversationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "selfTriggeredBot": false
}

Response: 200.

POST /conversations/deviceInfo — Send Device Info (Optional)

{
  "webSessionId": "k9Xj2mNpQr5sTv8wYz1AbC",
  "device_model": "Chrome 120",
  "device_brand": "Desktop",
  "os_version": "Windows 10",
  "browser_version": "120.0"
}

The backend may add IP and User-Agent from request headers. Response: 200.

POST /conversations/getStartupData — Check if Startup Data Exists

{
  "webSessionId": "k9Xj2mNpQr5sTv8wYz1AbC"
}

Response:

{
  "startupDataAvailable": true
}

POST /conversations/reset — Reset Conversation

{
  "webSessionId": "k9Xj2mNpQr5sTv8wYz1AbC",
  "conversationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}

Clears the conversation and deletes the dialog session. Response: 200. The backend also sends a realtime guest.webchat.clear for this conversation.

POST /conversations/messages/delete — Delete All Messages in Conversation

{
  "webSessionId": "k9Xj2mNpQr5sTv8wYz1AbC",
  "conversationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}

Response: 204. The backend sends realtime guest.webchat.clear with { "conversationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" }.

POST /users/customId — Set Custom User Id

{
  "webSessionId": "k9Xj2mNpQr5sTv8wYz1AbC",
  "customId": "ext-user-789"
}

Response: 200.

POST /preferences/get — Get User Preferences

{
  "webSessionId": "k9Xj2mNpQr5sTv8wYz1AbC"
}

Response:

{
  "language": "en"
}

POST /preferences — Set Language

{
  "webSessionId": "k9Xj2mNpQr5sTv8wYz1AbC",
  "language": "en"
}

Language must be one of the bot's languages (from /botInfo). Response: 200.

POST /saveFeedback — Save Message Feedback

{
  "messageId": "msg_7f8e9d0c-1b2a-3456-7890-abcdef123456",
  "target": "u_8k2Lm4nP6qR9sT1vWx3Yz",
  "feedback": 1
}

target: the userId for the visitor (from /conversations/list or /conversations/get). feedback: typically 1 (positive) or -1 (negative); check your backend for allowed values. Response: 200.

POST /feedbackInfo — Get Feedback for Messages

{
  "target": "u_8k2Lm4nP6qR9sT1vWx3Yz",
  "messageIds": [
    "msg_7f8e9d0c-1b2a-3456-7890-abcdef123456",
    "msg_8a9b0c1d-2e3f-4567-8901-bcdef2345678"
  ]
}

Response (example):

[
  { "messageId": "msg_7f8e9d0c-1b2a-3456-7890-abcdef123456", "feedback": 1 },
  { "messageId": "msg_8a9b0c1d-2e3f-4567-8901-bcdef2345678", "feedback": -1 }
]

Exact shape may vary; use it to show thumbs up/down or other feedback state per message.

GET /getTokenForClient — Get Auth Token

Example: GET https://your-host.com/api/v1/bots/welcome-bot/mod/channel-web/getTokenForClient?forcefull=false

Query: forcefulltrue to force a new token, false to reuse a cached one if valid.

Response:

{
  "authCode": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Realtime (Socket.IO)

Receiving Bot Messages and Events

The server sends to the guest socket in the room visitor:{visitorId} — your client is in that room because it connected with that visitorId.

Socket event name from server: event.

Payload shape: { name: string, data: object }, where name is one of guest.webchat.message, guest.webchat.typing, guest.webchat.data, guest.webchat.clear, and data is the event payload (may include __room, safe to ignore in the UI).

{
  "name": "guest.webchat.message",
  "data": {
    "id": "msg_003",
    "conversationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "authorId": null,
    "sentOn": "2025-02-19T10:30:16.000Z",
    "payload": { "type": "text", "text": "Sure! Could you share your order number?" },
    "__room": "visitor:u_8k2Lm4nP6qR9sT1vWx3Yz"
  }
}

guest.webchat.message

A message object (same shape as in /conversations/get). authorId present → user message; null/absent → bot or agent message.

  • Web channel payload.type: text, quick_reply, form, image, file, card, carousel, typing, etc. — see Message Payload Types.
  • Agent handoff: messages may include payload.handoverStatus — see Agent Handoff.
  • Click-to-call: messages may include payload.clickToCallEvent set to 'video' or 'voice' — see Click-to-Call.
{
  "id": "msg_abc",
  "conversationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "authorId": null,
  "sentOn": "2025-02-19T10:35:22.000Z",
  "payload": { "type": "text", "text": "Here is the link you asked for: https://example.com/help" },
  "__room": "visitor:u_8k2Lm4nP6qR9sT1vWx3Yz"
}

guest.webchat.typing

{
  "timeInMs": 500,
  "conversationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "__room": "visitor:u_8k2Lm4nP6qR9sT1vWx3Yz"
}

Show "bot is typing" for timeInMs milliseconds (or until the next message in that conversation).

guest.webchat.data

Arbitrary payload from the bot (e.g. custom data). No fixed schema.

{
  "customField": "value",
  "metadata": { "source": "flow", "nodeId": "abc" },
  "__room": "visitor:u_8k2Lm4nP6qR9sT1vWx3Yz"
}

guest.webchat.clear

{
  "conversationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "__room": "visitor:u_8k2Lm4nP6qR9sT1vWx3Yz"
}

Clear the message list for that conversation in your UI (e.g. after reset or delete-messages).

Step-by-Step Integration Flow

One-Time Setup (App Load)

StepWhat to doWhat you have after
1Get botId, baseUrl, ROOT_PATH from config.Configuration ready.
2Get or create visitorId (localStorage; generate with nanoid(24) if missing).A stable visitorId for this browser.
3Connect Socket.IO to {baseUrl}/guest with path: ROOT_PATH + '/socket.io', query: { visitorId, botId }, transports: ['websocket', 'polling'].Socket connection in progress.
4On socket connect, store webSessionId = socket.id.webSessionId ready to use in REST.
5Register socket.on('event', ({ name, data }) => { ... }) and handle guest.webchat.message, guest.webchat.typing, guest.webchat.data, guest.webchat.clear.Realtime events will update your UI on bot replies or status changes.

Do not call any channel-web REST endpoint (other than GET /botInfo) until step 4 has run. Otherwise the server rejects the request.

Loading the Chat (User Opens the Chat UI)

StepWhat to doWhat you have after
6(Optional) Call GET /botInfo to get bot name, theme, languages, startup form, etc.Bot config for your UI.
7Call POST /conversations/list with { webSessionId, startupData }.List of conversations with id, createdOn, lastMessage.
8To show an existing conversation: call POST /conversations/get with a chosen id. For a new chat: call POST /conversations/new and use the returned convoId.A conversationId and a list of messages to show (or empty for a new conversation).
9(Optional) If the widget just opened, call POST /events with payload: { type: 'visit' }.Backend notified the user opened the chat — may trigger a welcome message via guest.webchat.message.

Sending a Message

StepWhat to doWhat you have after
10Call POST /messages with { webSessionId, conversationId, payload: { type: 'text', text: '...' }, isEmulator: false }. You can omit conversationId to use/create the most recent conversation.HTTP 200. Your UI can optimistically add the user message.
11Do not expect the bot reply in the POST response — it arrives as Socket.IO events: first guest.webchat.typing, then guest.webchat.message.Chat UI updated with the bot reply.

Optional Features

  • Startup form: POST /conversations/startupData with webSessionId, conversationId, name, email, phone, selfTriggeredBot.
  • File upload: POST /messages/files with FormData (file, webSessionId, conversationId). The file arrives as a message; you may get guest.webchat.message with payload.type === 'file'.
  • Reset conversation: POST /conversations/reset with webSessionId and conversationId. You'll receive guest.webchat.clear; clear the message list and optionally call POST /conversations/new for a new convoId.
  • Preferences: POST /preferences/get and POST /preferences with webSessionId (and language for set).

Quick Reference: Minimal Path to "Send One Message and See a Reply"

  1. Connect socket to /guest with visitorId and botId.
  2. On connect, set webSessionId = socket.id.
  3. Listen for socket.on('event', ...) and handle guest.webchat.message (append to UI).
  4. Call POST /conversations/new with { webSessionId } → get convoId.
  5. Call POST /messages with { webSessionId, conversationId: convoId, payload: { type: 'text', text: 'Hello' } }.
  6. Wait for guest.webchat.message on the socket; display the message in your UI.
Error Handling

Common Pitfalls

  • Calling REST before the socket is connected — always wait for socket.on('connect') and set webSessionId = socket.id before any channel-web POST.
  • Omitting webSessionId in the request body — every POST (except GET /botInfo) needs it in the JSON body, not the URL.
  • Sending the wrong Content-Type — use application/json for JSON POST bodies.
  • Using an old webSessionId after the socket reconnects — after a disconnect, socket.id changes; use the new id for all subsequent REST calls.

HTTP Errors

ErrorMeaning
401 / 403 or "session id is invalid"webSessionId is missing, wrong, or the socket disconnected. Reconnect and use the new socket.id.
400 "conversation ID doesn't belong to that user"The conversationId isn't owned by the user identified by this webSessionId. Use an id from /conversations/list or create a new one.
400 "`type` is required and must be valid"The payload.type in POST /messages must be one of the supported types — see Message Payload Types.

Socket Errors

If the socket disconnects, set webSessionId = null and don't call REST until reconnected; after reconnecting, use the new socket.id. If you never receive guest.webchat.message after sending a message, confirm you're listening for the event name event and reading data.name === 'guest.webchat.message' / data.data.

Agent Handoff and Click-to-Call

Agent Handoff Flow

When the bot hands the conversation to a human agent (HITL), the same channel-web conversation and realtime channel are used — no extra APIs for the user's webchat.

How Handoff Works

  1. Trigger: the bot (via flow/action) requests a handoff. The backend creates a handoff record associated with the current user conversation (userThreadId = current conversationId, userChannel = 'web').
  2. User sees: a guest.webchat.message with payload.handoverStatus: 'initiated' and optional payload.text (e.g. "Connecting you to an agent…"). Same conversation continues.
  3. During handoff: the user keeps sending messages via POST /messages (same conversationId). Agent messages arrive as guest.webchat.message and look like bot messages. Status updates arrive the same way with payload.handoverStatus set.
  4. History: POST /conversations/get returns the full merged thread (user + agent messages) in the same message shape.

Handoff Status Values (payload.handoverStatus)

ValueMeaningSuggested UI
initiatedHandoff requested; waiting for an agent"Connecting you to an agent…"
assignedAn agent has been assigned"An agent has been assigned."
acceptedAgent accepted the conversation"An agent is now with you."
handoverResolvedAgent ended the handoff; back to bot"Conversation with agent ended."
expiredNo agent took the conversation in time"No agent available. Try again later."
noAgentNo agents available"No agents available."
disconnectedHandoff was disconnected"Disconnected."

Show these as status lines/banners above or below the message list, or as system messages. Your frontend can hide handoff-only messages from a downloadable transcript, same as the default UI.

Optional: Guest Socket with Handoff Context

If the user opens webchat from a link that already has a handoff id (e.g. from an agent dashboard), connect the guest socket with handoffId in the query. A normal user starting the chat from your site does not need to send handoffId — they receive handoff status and agent messages via the same guest.webchat.message events.

Agent-side APIs are out of scope here

Assigning, accepting, resolving, and listing handoffs are done via the hitlnext module (e.g. POST /api/v1/bots/:botId/mod/hitlnext/handoffs/...) — those are agent-dashboard APIs, not part of the end-user webchat integration on this page.

Click-to-Call

Click-to-call lets the user start a voice or video call from the web chat UI. It's part of the web channel: the same conversation, socket, and REST APIs are used — the backend only signals when to show/start the call.

StepAction
1Call GET /botInfo; if clickToCall is 'video' or 'voice', show a call/video-call button.
2On chat open, send POST /events with payload: { type: 'visit' }. If click-to-call is enabled, the backend sends a message on the same conversation with payload.clickToCallEvent set to 'video' or 'voice'.
3In your socket.on('event', ...) handler, on guest.webchat.message, check data.payload.clickToCallEvent; if set, show or start the call UI. The message text (e.g. "Hi") can be hidden from the chat bubble list. The backend may resend this signal after a previous handoff ends (e.g. handoverResolved).
4Implement the actual call with your own voice/video stack (WebRTC, SIP, or third-party SDK) — channel-web only provides the conversation/session context and the signal, not the call media itself.
Reference

CORS and Same-Origin

The channel-web router is created with checkAuthentication: false for the bot, but the server must still allow requests from your frontend origin (CORS). Configure CORS on the chatbot-engine server so your external frontend's origin is allowed. For Socket.IO, use the same origin if the frontend is served from the same host; otherwise ensure the server allows your origin for the Socket.IO transport.

Message Payload Types

User → Backend (IN) for POST /messages:

typeFull payload example
text{ "type": "text", "text": "I need help with my order" }
quick_reply{ "type": "quick_reply", "text": "Track order", "payload": "track" }
postback{ "type": "postback", "text": "Track order", "payload": "track" }
form{ "type": "form", "form": "contact-form", "data": { "name": "Jane Doe", "email": "[email protected]", "message": "I need help with my order" } }
login_prompt{ "type": "login_prompt" }
visit{ "type": "visit" } (typically sent via POST /events instead — see POST /events)
request_start_conversation{ "type": "request_start_conversation" }

Backend → Client (OUT) in payload:

typeFull payload example
text{ "type": "text", "text": "Here is the information you requested." }
quick_reply{ "type": "quick_reply", "text": "Choose one:", "choices": [ { "title": "Yes", "value": "yes" }, { "title": "No", "value": "no" } ] }
image{ "type": "image", "image": "https://cdn.example.com/photo.jpg", "title": "Photo" }
file{ "type": "file", "url": "https://cdn.example.com/doc.pdf", "title": "Document.pdf" }
card{ "type": "card", "title": "Product", "subtitle": "Description", "image": "https://cdn.example.com/product.jpg", "actions": [ { "title": "Buy", "value": "buy_123" } ] }
carousel{ "type": "carousel", "items": [ { "title": "Item 1", "subtitle": "Desc 1", "image": "https://cdn.example.com/item1.jpg", "actions": [ { "title": "Select", "value": "item_1" } ] } ] }
typing{ "type": "typing", "value": 500 } (often sent as the separate guest.webchat.typing event instead)
Handoff{ "type": "text", "text": "Connecting you to an agent…", "handoverStatus": "initiated" } — other statuses: assigned, accepted, handoverResolved, expired, noAgent, disconnected
Click-to-call{ "type": "text", "text": "Hi", "clickToCallEvent": "video" } or "clickToCallEvent": "voice"

Minimal Integration Checklist

StepAction
1Connect Socket.IO to /guest with visitorId + botId; store webSessionId = socket.id.
2Include webSessionId in the body of all channel-web POSTs that require user identity.
3Listen to socket.on('event', ({ name, data }) => ...) and handle guest.webchat.message (incl. handoverStatus and clickToCallEvent), guest.webchat.typing, guest.webchat.data, guest.webchat.clear.
4Use POST /conversations/list and POST /conversations/get to load history; POST /conversations/new for a new chat.
5Send messages with POST /messages (body: webSessionId, conversationId, payload: { type, text }).
6Optional: POST /events with type: 'visit' on open; startup form, file upload, reset, preferences.
7Optional: when opening chat in a handoff context, connect the guest socket with query handoffId; handle handoff status in messages.
8Optional: click-to-call — use GET /botInfoclickToCall to show a call button; handle guest.webchat.message with payload.clickToCallEvent to show/start the call UI.

Standalone voice, WhatsApp, and other non-web channels are covered on their own pages, not here.

Next Steps

Build with the SDK or UI Kit

Now that you understand the flow, pick a starting point for your platform:

Chatbot SDK — build your own UI

Chatbot UI Kit — pre-built UI