Headless Chat SDK
The Yellow Chat SDK (@yellowdotai/yellow-chat-sdk) is a TypeScript SDK for integrating Yellow.ai chat into your own web application with a completely custom UI. It provides an event-based communication layer, giving you full separation between the data layer (handled by the SDK) and your UI implementation.
Use it when you want your own look-and-feel instead of the standard embedded chat widget, while still using Yellow.ai's backend for bot and live-agent conversations.
Installation
npm install @yellowdotai/yellow-chat-sdk
Or with yarn:
yarn add @yellowdotai/yellow-chat-sdk
Quick start
import { YellowChat } from "@yellowdotai/yellow-chat-sdk";
const sdk = new YellowChat();
// Subscribe to events BEFORE connecting
sdk.on("connection:connected", ({ userId }) => {
console.log("Connected as:", userId);
});
sdk.on("message:received", (message) => {
// message = { id, timestamp, type, content, sender }
console.log("New message:", message);
});
// Initialize and connect
await sdk.init({
bot: "your-bot-id",
host: "https://cloud.yellow.ai",
});
await sdk.connect();
await sdk.sendMessage("Hello!");
Always subscribe to events before calling
connect(), otherwise you may miss early messages.
Configuration
Pass configuration to sdk.init():
interface SDKConfig {
// Required
bot: string; // Your bot ID (e.g. 'x1632218421575')
// Server
host?: string; // API host URL (default: 'https://cloud.yellow.ai')
// Message source (important for live agent integrations - see below)
source?: string; // 'yellowmessenger' (default) or 'syncApi'
// User
userId?: string; // Pre-set user ID (auto-generated if omitted)
name?: string; // User display name
// Authentication
ymAuthenticationToken?: string; // Auth token for verified users (if required by your bot)
// Custom payload / journeys
payload?: Record<string, unknown>;
triggerJourney?: string;
// UTM parameters (analytics)
utmSource?: string;
utmCampaign?: string;
utmMedium?: string;
// Debug
debug?: boolean; // Enable debug logging (default: false)
}
How to find your host value
The host must exactly match the one shown in your bot's Chat Widget deployment script:
- Log in to cloud.yellow.ai.
- Go to Extensions → Chat Widget → Deploy.
- Copy the
hostvalue from the deployment script (e.g.https://cloud.yellow.ai).
Using a different host than the one in the deployment script can cause connectivity or authentication issues.
Source configuration
The source parameter determines how messages are routed through Yellow.ai's backend:
| Source | Use case |
|---|---|
yellowmessenger | Default. Fully headless, SDK-only integrations where the SDK handles all communication (send + receive) over the widget/WebSocket channel. |
syncApi | Hybrid mode — when you send bot messages via the /integrations/sync/v1/message REST API and switch to the SDK when a live agent connects. |
// Fully headless - SDK only (default)
await sdk.init({ bot: "your-bot-id", source: "yellowmessenger" });
// Hybrid mode - REST API + SDK for live agent
await sdk.init({ bot: "your-bot-id", source: "syncApi" });
Important: In hybrid mode you must set
source: 'syncApi'and connect with the same user id you used assenderin the REST API calls, so the SDK stays on the same ticket. Mismatched sources/ids cause live-agent tickets to close or messages to be lost.
Live agent (agent inbox): use yellowmessenger
If your custom UI needs to receive live-agent messages (agent inbox), use source: 'yellowmessenger' and send all messages through the SDK (sdk.sendMessage(...)). This keeps sending and receiving on the same widget/WebSocket channel, which is how the backend routes agent replies — and ticket lifecycle events like ticket:resolved — back to your widget over XMPP.
await sdk.init({ bot: "your-bot-id", source: "yellowmessenger" });
await sdk.connect(userId);
await sdk.sendMessage("Hi"); // agent replies arrive on 'message:received'
Why not
syncApifor agent chat? The sync API (/integrations/sync/v1/message) is a request/response channel — the agent's reply comes later, asynchronously, and there is no open channel on the sync side to push it back. So insyncApimode, agent → user messages are not guaranteed to reach the SDK. They arrive only if (a) the same user id is used for the syncsenderandsdk.connect(userId), and (b) the platform is configured to route that bot's agent messages to XMPP. If you must keep sending via the sync API, either arrange that backend routing or poll conversation history to fetch agent replies. For a purely headless widget that must show agent messages, preferyellowmessenger.
Domain check (accepted domains)
If the current site's domain is not in the bot's accepted domains, the SDK throws a YellowChatError with code DOMAIN_NOT_ALLOWED and does not open the WebSocket. Configure allowed domains at cloud.yellow.ai → Extensions → Chat Widget → Secure Bot → Web.
sdk.on("connection:error", (error) => {
if (error.code === "DOMAIN_NOT_ALLOWED") {
console.error("This site is not allowed to use this bot:", error.message);
}
});
API reference
Lifecycle
await sdk.init(config); // Initialize (must be called first)
await sdk.connect(userId?); // Connect (userId optional)
sdk.disconnect(); // Disconnect
connect() resolves the user id in this order: the provided userId → a stored UID from localStorage → an anonymous, server-assigned UID.
Messaging
// Send a text message
await sdk.sendMessage("Hello!", { title?, metadata?, skipRateLimit? });
Returns a MessageResponse with the message id, trace id, and timestamp.
File upload
sendFile(file: File): Promise<FileUploadResponse>
Uploads a File and posts it into the conversation in a single call — the SDK uploads the file to the server and then delivers the file message into the conversation, so both the bot/agent and your own UI receive it. Works in both bot and live-agent conversations.
const input = document.querySelector('input[type="file"]');
input.addEventListener("change", async (e) => {
const file = e.target.files[0];
if (!file) return;
try {
const result = await sdk.sendFile(file); // { url, name, size }
console.log("Uploaded:", result.url);
} catch (error) {
console.error("Upload failed:", error);
}
});
// Track status via events
sdk.on("file:uploadStarted", ({ fileId, fileName }) => showSpinner(fileName));
sdk.on("file:uploadCompleted", ({ fileId, file }) => showPreview(file.url));
sdk.on("file:uploadFailed", ({ fileId, error }) => showError(error.message));
interface FileUploadResponse {
url: string; // Hosted URL of the uploaded file
name: string; // Original file name
size: number; // File size in bytes
}
Files are uploaded as
multipart/form-data; do not set aContent-Typeheader manually (the SDK does this). Failures throw aYellowChatErrorwith codeFILE_UPLOAD_FAILEDorFILE_TOO_LARGE.
State queries
sdk.isConnected(); // boolean
sdk.getUserId(); // string | null
sdk.getBotInfo(); // BotInfo | null (name, icon, ...)
Events
const unsubscribe = sdk.on("message:received", (message) => { /* ... */ });
unsubscribe(); // stop listening
sdk.off("message:received", handler); // or unsubscribe by reference
Events reference
Connection events
| Event | Payload | Description |
|---|---|---|
connection:connecting | – | Connection attempt started |
connection:connected | { userId } | Successfully connected |
connection:disconnected | DisconnectReason | Disconnected |
connection:reconnecting | { attempt, delay } | Attempting to reconnect |
connection:reconnected | { userId } | Successfully reconnected |
connection:reconnectFailed | – | All reconnection attempts failed |
connection:error | SDKError | Connection error |
Message events
| Event | Payload | Description |
|---|---|---|
message:received | IncomingMessage | New message received |
message:sent | { id, message } | Message sent successfully |
message:failed | { messageId, error } | Message send failed |
message:typing | boolean | Typing indicator status |
Agent events
| Event | Payload | Description |
|---|---|---|
agent:assigned | AgentProfile | Live agent assigned to the conversation |
agent:left | – | Agent left or ticket closed/resolved |
agent:typing | boolean | Agent is typing |
Ticket events
| Event | Payload | Description |
|---|---|---|
ticket:created | { ticketId } | Support ticket created |
ticket:closed | – | Ticket/conversation closed |
ticket:resolved | – | Ticket resolved |
ticket:transferred | { newAgent } | Ticket transferred to a new agent |
File events
| Event | Payload | Description |
|---|---|---|
file:uploadStarted | { fileId, fileName } | File upload started |
file:uploadProgress | { fileId, progress } | Upload progress (0–100), if supported |
file:uploadCompleted | { fileId, file } | Upload completed (file = { url, name, size }) |
file:uploadFailed | { fileId, error } | Upload failed |
History & network events
| Event | Payload | Description |
|---|---|---|
history:previousMessages | IncomingMessage[] | Previous messages (auto-fetched on connect; newest first) |
network:online | – | Network connection restored |
network:offline | – | Network connection lost |
Message types
All messages from the SDK use a standardized, flattened format — internal metadata is stripped, so you only receive what your UI needs:
interface IncomingMessage {
id: string;
timestamp: Date;
type: MessageType; // 'text' | 'image' | 'video' | 'audio' | 'file' | 'quickReplies' | 'cards' | 'location' | 'datePicker' | 'feedback' | 'filePrompt' | 'system'
content: MessageContent;
sender: "bot" | "agent" | "user" | "system";
}
Content by type:
// text / system : { message: string }
// image / audio : { url: string }
// video : { url, controls, autoplay, loop, muted, downloadable, thumbnail }
// file : { url, name, size?, mimeType? }
// quickReplies : { quickReplies: { title?, options[] } }
// cards : { cards: Card[] }
// location : { title?, options[], showMap, mapsApiKey } | { lat, long }
// datePicker : { display: 'date' | 'dateTime' | 'time' | 'month' | 'range' }
// feedback : { title, surveyType, detailedFeedback?, ... }
// filePrompt : { message, allowMultiple, allowSkip, safeguardUploadedFile }
System messages are generated automatically for: agent joined ("[Agent] joined the conversation"), ticket closed ("Conversation ended"), and ticket resolved ("Ticket has been resolved").
Handling messages
sdk.on("message:received", (message) => {
switch (message.type) {
case "text":
renderText(message.content.message, message.sender);
break;
case "image":
renderImage(message.content.url);
break;
case "file":
renderFileDownload(message.content.url, message.content.name);
break;
case "quickReplies":
renderQuickReplies(message.content.quickReplies);
break;
case "system":
renderSystemMessage(message.content.message);
break;
// ... handle remaining types
}
});
Error handling
import { YellowChatError } from "@yellowdotai/yellow-chat-sdk";
try {
await sdk.sendMessage("Hello!");
} catch (error) {
if (error instanceof YellowChatError) {
console.error(error.code, error.message);
}
}
sdk.on("connection:error", (error) => console.error(error.code, error.message));
sdk.on("message:failed", ({ messageId, error }) => markFailed(messageId, error));
Common error codes: CONNECTION_FAILED, CONNECTION_TIMEOUT, AUTH_FAILED, MESSAGE_SEND_FAILED, MESSAGE_RATE_LIMITED, FILE_UPLOAD_FAILED, FILE_TOO_LARGE, BOT_LOAD_FAILED, DOMAIN_NOT_ALLOWED, NETWORK_ERROR, INVALID_CONFIG.
Best practices
- Subscribe to events before
connect()so you don't miss early messages. - Handle reconnection via
connection:reconnecting/connection:reconnected— the SDK auto-reconnects and re-fetches missed history. - Check
isConnected()before sending. - Clean up on unmount: call the unsubscribe functions returned by
sdk.on(...)andsdk.disconnect().
Troubleshooting
- Connection fails immediately — verify the bot id, that
hostmatches the deployment script, and that your domain is in the bot's accepted domains. DOMAIN_NOT_ALLOWED— add your site's domain under Extensions → Chat Widget → Secure Bot → Web.- CORS errors on a customer domain — ensure you're using the latest SDK version; the SDK identifies the bot via the
?bot=query param and avoids custom headers that some gateway routes don't allow.
Support
For issues and feature requests, contact Yellow.ai support.