Exotel Chat SDK — Integration Reference
Overview
The Exotel Chat SDK (exotel-chat-sdk.js) is a JavaScript library that embeds the Exotel Live Chat widget into any web page. It exposes a global window.exochat object with methods for starting chat sessions, sending and receiving messages, handling file uploads, read receipts, and CSAT feedback.
This reference is intended for third-party chatbot and web application developers who need to integrate with the Exotel Chat widget — for example, to hand off bot sessions to live agents, exchange messages programmatically, or embed the widget in a custom CRM or portal.
How It Works
- A web page or chatbot application includes exotel-chat-sdk.js via a <script> tag.
- On DOMContentLoaded, the host page calls exochat.initialize(config) with the Exotel tenant credentials and campaign settings.
- When a customer (or bot) is ready to chat, the host calls exochat.startChat(params). This call:
- POSTs to the Exotel Chat REST API to provision an XMPP user and MUC room for the session.
- Dynamically loads Converse.js (XMPP client library) and connects over BOSH.
- Joins the provisioned MUC room, where an agent (or queue) is waiting.
- Incoming messages are received via the XMPP listener and surfaced through the exochat.on("messageReceived", callback) event.
- At the end of the session, the agent closes the chat. The SDK receives a chatEnded message and (optionally) presents a CSAT feedback form.
Prerequisites
- The Exotel Chat service must be deployed and reachable at chat_service_domain (default port 4440).
- The CMS service must be reachable at cms_service_domain (default port 4433) for feedback configuration.
- The BOSH endpoint (converse.bosh_service_url) must be accessible from the customer's browser.
- A valid auth_token (RestrictedToken <token>) must be provided for REST API calls.
- A live campaign with at least one configured nodeflow must exist (campaignId, nodeflowId).
Embedding the SDK
<!-- Include the SDK -->
<script src="https://your-host/exotel-chat-sdk.js" defer></script>
<script>
document.addEventListener("DOMContentLoaded", () => {
window.exochat.initialize({
chat_service_domain: "https://omni.example.com:4440",
cms_service_domain: "https://omni.example.com:4433",
converse: {
bosh_service_url: "https://omni.example.com:4442/http-bind/"
},
auth_token: "RestrictedToken <your_token>",
contactCenterId: 1,
campaignId: 10,
nodeflowId: 3,
processId: 2,
locale: "en",
show_controlbox_by_default: true,
});
});
</script>exochat.initialize(config)
Initialises the SDK, merges the provided values over defaults, and injects the chat icon into the DOM. Must be called once before any other method.
Signature: exochat.initialize(config: ExochatConfig): void
Configuration parameters
Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
chat_service_domain | string | Yes | — | Base URL of the Exotel chat service (port 4440). E.g. https://omni.example.com:4440 |
cms_service_domain | string | Yes | — | Base URL of the CMS service (port 4433). Used for feedback config lookup. |
converse.bosh_service_url | string | Yes | — | BOSH endpoint for XMPP. E.g. https://omni.example.com:4442/http-bind/ |
auth_token | string | Yes | — | Authorization header value. Format: "RestrictedToken <token>" |
contactCenterId | number | Yes | — | Numeric ID of the contact center. |
campaignId | number | Yes | 1 | Campaign to route the chat into. |
nodeflowId | number | Yes | 5 | Nodeflow (IVA) ID for routing logic. |
processId | number | Yes | 4 | Process ID. Used for feedback config lookup. |
locale | string | No | "en" | UI locale for the chat widget. E.g. "en", "hi". |
accountId | string | null | No | null | Optional account ID sent as a request header for multi-tenant setups. |
theme_color | string | No | "#192D3F" | Primary brand color for the chat widget UI. |
show_controlbox_by_default | boolean | No | true | Whether to show the chat icon immediately on load. |
view_mode | string | No | "" | Display mode for the widget layout. |
Methods
All methods are available on window.exochat after initialize() is called.
Session Management
startChat(params)
Creates an XMPP user and MUC room via the Exotel REST API, then loads Converse.js and joins the room. This is the primary method for bot-to-agent handoff.
Signature: exochat.startChat(params: StartChatParams): Promise<Room>
Input parameters
Field | Type | Required | Description |
|---|---|---|---|
fullName | string | Yes | Customer display name shown to the agent. |
string | Yes | Customer email. Stored as a user property on the session. | |
phone | string | Yes | Customer phone number. Stored as a user property on the session. |
user_id | string | null | No | Bot user ID (for bot-to-agent transfers). Setting this marks the session as bot-transferred. |
session_id | string | null | No | Bot session ID (for bot-to-agent transfers). Passed as botSessionId. |
...additionalInfo | Record<string, any> | No | Any additional key-value pairs forwarded as additionalInfo to the REST API (e.g. custom CRM fields, intent, language). |
Internal REST call
Endpoint: POST {chat_service_domain}/plugins/restapi/v1/xtrm/users/connection
Request headers:
{
"Content-Type": "application/json",
"Authorization": "<config.auth_token>",
"contactCenterId": "<config.contactCenterId>",
"accountId": "<config.accountId>"
}Request body:
{
"requestId": "uuid-v4",
"userEntity": {
"name": "<fullName>",
"properties": [
{ "key": "email", "value": "<email>" },
{ "key": "phone", "value": "<phone>" },
{ "key": "nodeflowId", "value": "<config.nodeflowId>" },
{ "key": "locale", "value": "<config.locale>" }
]
},
"serviceName": "conference",
"campaignId": "<config.campaignId>",
"sendInvitations": true,
"botTransferred": false,
"additionalInfo": { },
"botSessionInfo": {
"userId": "<user_id | null>",
"botSessionId": "<session_id | null>"
}
}Successful response (200 OK):
{
"userEntity": {
"username": "xmpp_user@domain",
"password": "xmpp_password",
"name": "Customer Display Name"
},
"mucRoomEntity": {
"roomName": "room_abc123",
"members": ["user@domain", "agent@domain"],
"owners": ["agent@domain"]
},
"exportTranscriptAllowed": true
}Promise resolution: On success, the Promise resolves with the Converse Room object returned by joinRoom(). On failure, it rejects with one of:
Error shape | Cause |
|---|---|
{ errorData, roomJid, customerJid } | Failed to connect to XMPP/BOSH server after REST succeeded |
string (HTTP statusText) | REST API returned a non-2xx status |
Error object | Network / fetch exception |
Bot-to-agent handoff example
exochat.startChat({
fullName: "Jane Smith",
email: "[email protected]",
phone: "+919988776655",
user_id: "bot_user_id_123",
session_id: "bot_session_abc",
intent: "billing_issue",
language: "en",
}).then((room) => {
console.log("Handoff complete:", room);
}).catch((err) => {
console.error("Handoff failed:", err.errorData || err);
});logout()
Logs the XMPP user out of the Converse session.
Signature: exochat.logout(): void
getUserStatus()
Returns the current XMPP presence status of the user (e.g. "online", "away").
Signature: exochat.getUserStatus(): string
Room Management
createRoom(roomJid, nick)
Creates a new MUC room in Converse.
Signature: exochat.createRoom(roomJid: string, nick: string): Promise<void>
Parameter | Type | Description |
|---|---|---|
roomJid | string | Full MUC JID of the room (e.g. [email protected]). |
nick | string | Nickname to use inside the room. |
joinRoom(roomJid, nick)
Opens and joins an existing MUC room.
Signature: exochat.joinRoom(roomJid: string, nick: string): Promise<Room>
leaveRoom(roomJid)
Closes (leaves) a MUC room in Converse.
Signature: exochat.leaveRoom(roomJid: string): void
Messaging
sendMessage(roomJid, body, type, files)
Sends a message to the specified MUC room.
Signature: exochat.sendMessage(roomJid: string, body: string, type: string, files: FileAttachment[] | null): void
Parameter | Type | Description |
|---|---|---|
roomJid | string | Full MUC JID of the destination room. |
body | string | Message text (or file URL when type is MEDIA_MSG). |
type | string | "NORMAL_TEXT_MSG" for plain text; "MEDIA_MSG" for file attachments. |
files | FileAttachment[] | null | Array of file objects when type is MEDIA_MSG; null for text messages. |
// Send a text message
exochat.sendMessage(roomJid, "Hello, how can I help?", "NORMAL_TEXT_MSG", null);
// Send a file attachment
exochat.sendMessage(roomJid, fileUrl, "MEDIA_MSG", [{ url: fileUrl, name: "report.pdf" }]);receiveMessage(message)
Triggers the "messageReceived" event and returns the message object. Typically called internally by the XMPP listener; can be called manually to inject messages.
Signature: exochat.receiveMessage(message: MessageObject): MessageObject
fetchArchivedMessages(roomJid)
Fetches the message history for a room from the REST API and replays it into the widget's message list.
Signature: exochat.fetchArchivedMessages(roomJid: string): Promise<void>
Internal REST call: GET {chat_service_domain}/plugins/restapi/v1/xtrm/chatrooms/{roomJid}/messages
Typing Indicators
sendTypingStatus(roomJid)
Sends an XMPP "composing" chat state to the room (the local user is actively typing).
Signature: exochat.sendTypingStatus(roomJid: string): void
sendPausedStatus(roomJid)
Sends an XMPP "paused" chat state to the room (the local user has stopped typing).
Signature: exochat.sendPausedStatus(roomJid: string): void
Read Receipts
sendDisplayedReceipt(roomJid, msgid)
Sends an XEP-0333 "displayed" marker acknowledging that the customer has read a message. Also triggers the "displayedReceipt" event.
Signature: exochat.sendDisplayedReceipt(roomJid: string, msgid: string): void
sendReceiveReceipt(roomJid, msgid)
Sends an XEP-0184 "received" receipt confirming message delivery to the client. Also triggers the "receiveReceipt" event.
Signature: exochat.sendReceiveReceipt(roomJid: string, msgid: string): void
UI Control
showConverseChat(e?)
Shows the Converse chat panel within the widget.
Signature: exochat.showConverseChat(e?: any): void
hideConverseChat(e?)
Hides the Converse chat panel.
Signature: exochat.hideConverseChat(e?: any): void
disconnectConverse(e?)
Disconnects the active XMPP session (returns a Promise).
Signature: exochat.disconnectConverse(e?: any): Promise<void>
File Upload (XEP-0363 HTTP Upload)
Files are uploaded through the XMPP HTTP Upload extension. Use uploadFileToConverse() for the simplest integration.
uploadFileToConverse(file) (recommended)
Runs the full upload flow — discovers the upload service, requests a slot, uploads the file, and returns the public GET URL.
Signature: exochat.uploadFileToConverse(file: File): Promise<string | null>
Returns: The public URL of the uploaded file on success; null on failure.
const file = fileInput.files[0];
const publicUrl = await exochat.uploadFileToConverse(file);
if (publicUrl) {
exochat.sendMessage(roomJid, publicUrl, "MEDIA_MSG", [{ url: publicUrl, name: file.name }]);
} else {
console.error("File upload failed");
}requestSlotFromConverse(filename, size, contentType)
Requests an HTTP upload slot from the XMPP server component (XEP-0363 IQ).
Signature: exochat.requestSlotFromConverse(filename: string, size: number, contentType: string): Promise<{ putUrl: string, getUrl: string } | null>
uploadFile(file, putUrl, getUrl)
Uploads a File via HTTP PUT to the slot URL.
Signature: exochat.uploadFile(file: File, putUrl: string, getUrl: string): Promise<string | null>
Returns: getUrl on HTTP 200; null on error.
discoverFileUploadJID()
Discovers the JID of the XMPP HTTP Upload service component.
Signature: exochat.discoverFileUploadJID(): Promise<string | null>
requestUploadServiceInfo(jid)
Sends a disco#info IQ to the upload service JID.
Signature: exochat.requestUploadServiceInfo(jid: string): Promise<any>
URL Utility Methods
Method | Signature | Returns | Description |
|---|---|---|---|
isImageURL | isImageURL(url: string): boolean | boolean | true if the URL has an image extension (jpg, png, gif, webp, etc.). |
isAudioURL | isAudioURL(url: string): boolean | boolean | true if the URL has an audio extension (mp3, ogg, wav, etc.). |
isVideoURL | isVideoURL(url: string): boolean | boolean | true if the URL has a video extension (mp4, webm, etc.). |
isDocumentPdf | isDocumentPdf(url: string): boolean | boolean | true if the URL ends with .pdf. |
getFileName | getFileName(url: string): string | string | Extracts the filename from a URL path. |
Feedback (CSAT)
getFeedbackConfig()
Fetches the feedback scheme configuration for the current campaign from the CMS.
Signature: exochat.getFeedbackConfig(): Promise<FeedbackConfig | null>
Internal REST call: GET {cms_service_domain}/configuration/cc-list/{contactCenterId}/process-list/{processId}/campaigns/{campaignId}/chat-feedback-schemes-configurations/{campaignId}_{nodeflowId}_webchat
Response shape:
{
"isFeedbackEnabled": true,
"openInNewTab": false,
"feedbackUrl": "https://feedback.example.com/form"
}submitFeedbackResponse(interactionId, chatId, feedback)
Posts the customer's CSAT rating to the chat service.
Signature: exochat.submitFeedbackResponse(interactionId: string, chatId: string, feedback: any): Promise<Response>
Endpoint: POST {chat_service_domain}/plugins/restapi/v1/xtrm/chat-feedback
Request headers:
{
"Content-Type": "application/json",
"Authorization": "<config.auth_token>",
"contactCenterId": "<config.contactCenterId>",
"accountId": "<config.accountId>"
}Request body:
{
"campaign_id": "<config.campaignId>",
"interaction_id": "<interactionId>",
"chat_id": "<chatId>",
"feedback": { }
}Event Subscription
on(eventName, callback)
Registers a callback for a named SDK event. Multiple callbacks per event are supported.
Signature: exochat.on(eventName: string, callback: Function): void
triggerEvent(eventName, ...args)
Internally used to emit events to all registered callbacks. Can also be called directly to fire events manually.
Signature: exochat.triggerEvent(eventName: string, ...args: any[]): void
Events
Subscribe with exochat.on(eventName, callback).
messageReceived
Fired every time a message arrives in the MUC room from the XMPP server.
exochat.on("messageReceived", (message) => {
if (message.chatEnded) {
// Session was closed by the agent
const { chatId, INTERACTION_ID } = message.interaction_data || {};
handleChatEnd(chatId, INTERACTION_ID);
return;
}
if (message.subType === "MEDIA_MSG" && message.files) {
message.files.forEach(f => renderAttachment(f.url, f.name));
return;
}
if (message.sender === "agent") {
renderAgentMessage(message.body, message.nick, message.time);
exochat.sendDisplayedReceipt(message.from_muc, message.msgid);
}
});Message Object schema
Field | Type | Description |
|---|---|---|
id | string | Unique XMPP stanza element ID. |
msgid | string | Application-level message ID used for receipts. |
body | string | Plain text content of the message. |
from | string | Full JID of the sender (e.g. [email protected]/nick). |
from_muc | string | Bare MUC JID (e.g. [email protected]). |
from_real_jid | string | Real un-masked JID of the sender. |
to | string | Recipient JID. |
nick | string | Sender's nickname inside the MUC room. |
occupant_id | string | Stable occupant ID (XEP-0421). |
sender | "agent" | "me" | "agent" for contact-centre-side messages; "me" for messages sent by this client. |
type | "groupchat" | XMPP message type. |
chat_state | "active" | "composing" | "paused" | "inactive" | XMPP chat state of the sender. |
time | ISO 8601 string | Message timestamp. |
received | timestamp | Time the message was received locally. |
message | string | Alias for body. |
subType | "NORMAL_TEXT_MSG" | "MEDIA_MSG" | "FORM_MSG" | null | Message sub-classification. MEDIA_MSG means file attachments are present. |
files | FileAttachment[] | null | Array of { url, name } objects when subType is MEDIA_MSG; null otherwise. |
userRole | "AGENT" | "BOT" | "SUPERVISOR" | null | Role of the sender on the contact-centre side. |
is_markable | boolean | Whether the SDK should send a "displayed" marker back for this message. |
is_marker | boolean | Whether this message is itself a read marker. |
marker_id | string | When is_marker is true, the ID of the message being acknowledged. |
receipt_id | string | ID for XEP-0184 receipt correlation. |
displayed | boolean | Client-side flag tracking whether "displayed" has been sent. Default: false. |
interaction_data | { chatId, INTERACTION_ID } | null | Interaction metadata. Populated when chatEnded is true. |
chatEnded | boolean | true when this message signals end-of-session (agent closed the chat). |
displayedReceipt
Fired when the SDK sends a "displayed" read marker for a message. Useful for updating delivery indicators in a custom UI.
exochat.on("displayedReceipt", (roomJid, msgid) => {
console.log("Displayed:", msgid, "in room:", roomJid);
});Argument | Type | Description |
|---|---|---|
roomJid | string | MUC JID where the receipt was sent. |
msgid | string | ID of the message that was displayed. |
receiveReceipt
Fired when the SDK sends an XEP-0184 "received" receipt confirming delivery to this client.
exochat.on("receiveReceipt", (roomJid, msgid) => {
console.log("Received receipt for:", msgid);
});Argument | Type | Description |
|---|---|---|
roomJid | string | MUC JID. |
msgid | string | ID of the acknowledged message. |
CSAT Feedback Flow
After a chat ends, the SDK automatically fetches feedback config and may display a form. To drive this flow from your own UI:
exochat.on("messageReceived", async (msg) => {
if (!msg.chatEnded) return;
const { chatId, INTERACTION_ID } = msg.interaction_data || {};
const config = await exochat.getFeedbackConfig();
if (!config?.isFeedbackEnabled) return;
if (config.openInNewTab) {
window.open(config.feedbackUrl, "_blank");
} else {
// Show your own rating UI, then on submit:
const rating = { score: 5, comment: "Great support!" };
await exochat.submitFeedbackResponse(INTERACTION_ID, chatId, rating);
console.log("Feedback submitted");
}
});Limitations
- The SDK dynamically loads Converse.js 10.1.4 from the Converse CDN (cdn.conversejs.org). The customer's browser must be able to reach that CDN, or the CDN must be mirrored internally.
- XMPP connectivity requires the bosh_service_url to be accessible from the customer's browser over HTTPS.
- The chat icon UI (#exotel-chat-icon) is injected into the host page's <body>. Custom CSS or z-index styles on the host page may affect its display.
- show_controlbox_by_default: false hides the icon on load; you must call showConverseChat() to show the widget programmatically.
- File upload uses XEP-0363 HTTP Upload and requires the XMPP server to have the upload service component configured.
- The view_mode parameter is accepted but the specific modes are not publicly documented; consult Exotel support for available values.