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
Prerequisites and Configuration
Before you start, you need:
| What | Description | Where to get it |
|---|---|---|
| Backend base URL | The root URL of your Dialogs/chatbot-engine server (e.g. https://chat.example.com). | From your deployment or backend team. |
| botId | The unique identifier of the bot users will chat with (e.g. welcome-bot, support-bot). | From bot configuration or your backend team. |
| ROOT_PATH | Optional 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.
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
| Term | Meaning |
|---|---|
| visitorId | A 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). |
| webSessionId | The 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. |
| conversationId | A UUID identifying one chat thread. You get it from POST /conversations/new (as convoId) or POST /conversations/list (each conversation has an id). |
| botId | The bot's unique id; used in the API path and in the socket query. |
| channel-web | The Dialogs module that serves the web chat REST API and works with the guest socket for realtime delivery. |
| guest socket | The 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-webmodule at{baseUrl}{ROOT_PATH}/api/v1/bots/{botId}/mod/channel-web - Realtime: Socket.IO namespace
/guest— connect to{baseUrl}/guestwithpath: {ROOT_PATH}/socket.io
- REST API: the
- Auth model: the frontend is identified by a guest Socket.IO connection. Every REST call must send
webSessionId(the guest socket id). The backend resolveswebSessionId→visitorId→userIdand uses that for conversations/messages. - chatbot-dependencies note: the
messaging/packages/webchatandmessaging/packages/socketmodules 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
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:
- 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.
- 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). - Wait for the socket to fire
connect. Only then readsocket.id— this is yourwebSessionId. If the socket disconnects, reconnect and use the newsocket.idfor 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:
| Field | Type | Required | Description |
|---|---|---|---|
webSessionId | string | Yes | Guest socket id (socket.id) |
Optional for specific routes: conversationId, payload, etc. — see each endpoint below.
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/jsonfor all POST requests with a JSON body (every endpoint except file upload). - Optional auth: for debugger / external auth, send
X-BP-ExternalAuth: Bearer {token}orExternalAuth: 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
| Field | Type | Required | Description |
|---|---|---|---|
webSessionId | string | Yes | Guest socket id |
conversationId | string (uuid) | No | Omit to use or create the most recent conversation |
isEmulator | boolean | No | Default false |
payload | object | Yes | Must 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:
| Field | Type | Required | Description |
|---|---|---|---|
file | File | Yes | The file to upload (image, PDF, etc.) |
webSessionId | string | Yes | Guest socket id |
conversationId | string (uuid) | Yes | Conversation to attach the file to |
payload | string | No | Optional 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: forcefull — true to force a new token, false to reuse a cached one if valid.
Response:
{
"authCode": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
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.clickToCallEventset 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).
One-Time Setup (App Load)
| Step | What to do | What you have after |
|---|---|---|
| 1 | Get botId, baseUrl, ROOT_PATH from config. | Configuration ready. |
| 2 | Get or create visitorId (localStorage; generate with nanoid(24) if missing). | A stable visitorId for this browser. |
| 3 | Connect Socket.IO to {baseUrl}/guest with path: ROOT_PATH + '/socket.io', query: { visitorId, botId }, transports: ['websocket', 'polling']. | Socket connection in progress. |
| 4 | On socket connect, store webSessionId = socket.id. | webSessionId ready to use in REST. |
| 5 | Register 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)
| Step | What to do | What you have after |
|---|---|---|
| 6 | (Optional) Call GET /botInfo to get bot name, theme, languages, startup form, etc. | Bot config for your UI. |
| 7 | Call POST /conversations/list with { webSessionId, startupData }. | List of conversations with id, createdOn, lastMessage. |
| 8 | To 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
| Step | What to do | What you have after |
|---|---|---|
| 10 | Call 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. |
| 11 | Do 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/startupDatawithwebSessionId,conversationId,name,email,phone,selfTriggeredBot. - File upload:
POST /messages/fileswith FormData (file,webSessionId,conversationId). The file arrives as a message; you may getguest.webchat.messagewithpayload.type === 'file'. - Reset conversation:
POST /conversations/resetwithwebSessionIdandconversationId. You'll receiveguest.webchat.clear; clear the message list and optionally callPOST /conversations/newfor a new convoId. - Preferences:
POST /preferences/getandPOST /preferenceswithwebSessionId(andlanguagefor set).
Quick Reference: Minimal Path to "Send One Message and See a Reply"
- Connect socket to
/guestwithvisitorIdandbotId. - On
connect, setwebSessionId = socket.id. - Listen for
socket.on('event', ...)and handleguest.webchat.message(append to UI). - Call
POST /conversations/newwith{ webSessionId }→ getconvoId. - Call
POST /messageswith{ webSessionId, conversationId: convoId, payload: { type: 'text', text: 'Hello' } }. - Wait for
guest.webchat.messageon the socket; display the message in your UI.
Common Pitfalls
- Calling REST before the socket is connected — always wait for
socket.on('connect')and setwebSessionId = socket.idbefore any channel-web POST. - Omitting
webSessionIdin the request body — every POST (exceptGET /botInfo) needs it in the JSON body, not the URL. - Sending the wrong
Content-Type— useapplication/jsonfor JSON POST bodies. - Using an old
webSessionIdafter the socket reconnects — after a disconnect,socket.idchanges; use the new id for all subsequent REST calls.
HTTP Errors
| Error | Meaning |
|---|---|
| 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 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
- Trigger: the bot (via flow/action) requests a handoff. The backend creates a handoff record associated with the current user conversation (
userThreadId= currentconversationId,userChannel='web'). - User sees: a
guest.webchat.messagewithpayload.handoverStatus: 'initiated'and optionalpayload.text(e.g. "Connecting you to an agent…"). Same conversation continues. - During handoff: the user keeps sending messages via
POST /messages(sameconversationId). Agent messages arrive asguest.webchat.messageand look like bot messages. Status updates arrive the same way withpayload.handoverStatusset. - History:
POST /conversations/getreturns the full merged thread (user + agent messages) in the same message shape.
Handoff Status Values (payload.handoverStatus)
| Value | Meaning | Suggested UI |
|---|---|---|
initiated | Handoff requested; waiting for an agent | "Connecting you to an agent…" |
assigned | An agent has been assigned | "An agent has been assigned." |
accepted | Agent accepted the conversation | "An agent is now with you." |
handoverResolved | Agent ended the handoff; back to bot | "Conversation with agent ended." |
expired | No agent took the conversation in time | "No agent available. Try again later." |
noAgent | No agents available | "No agents available." |
disconnected | Handoff 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.
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.
| Step | Action |
|---|---|
| 1 | Call GET /botInfo; if clickToCall is 'video' or 'voice', show a call/video-call button. |
| 2 | On 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'. |
| 3 | In 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). |
| 4 | Implement 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. |
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:
| type | Full 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:
| type | Full 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
| Step | Action |
|---|---|
| 1 | Connect Socket.IO to /guest with visitorId + botId; store webSessionId = socket.id. |
| 2 | Include webSessionId in the body of all channel-web POSTs that require user identity. |
| 3 | Listen to socket.on('event', ({ name, data }) => ...) and handle guest.webchat.message (incl. handoverStatus and clickToCallEvent), guest.webchat.typing, guest.webchat.data, guest.webchat.clear. |
| 4 | Use POST /conversations/list and POST /conversations/get to load history; POST /conversations/new for a new chat. |
| 5 | Send messages with POST /messages (body: webSessionId, conversationId, payload: { type, text }). |
| 6 | Optional: POST /events with type: 'visit' on open; startup form, file upload, reset, preferences. |
| 7 | Optional: when opening chat in a handoff context, connect the guest socket with query handoffId; handle handoff status in messages. |
| 8 | Optional: click-to-call — use GET /botInfo → clickToCall 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.
Build with the SDK or UI Kit
Now that you understand the flow, pick a starting point for your platform: