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 (see 'Authentication' below)
// 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);
}
});
Authentication
If your AI agent is configured for verified users, pass a token when you initialize the SDK:
await sdk.init({
bot: "your-bot-id",
ymAuthenticationToken: "<your-token>",
});
See YM authentication for how to generate the token.
How the token establishes identity
When ymAuthenticationToken is provided, the server validates it during init() and returns a feUID — the verified identity for that user. The SDK keeps this and uses it for the connection.
Because the verified identity comes from the token itself, the feUID takes priority over any userId you pass to connect():
await sdk.init({ bot: "your-bot-id", ymAuthenticationToken: token });
await sdk.connect(); // uses the feUID resolved from the token
await sdk.connect("user-123"); // "user-123" is ignored — the feUID still wins
Turn on
debug: truewhile integrating. The SDK logsfeUID received from serverwhen the token is accepted, andymAuthenticationToken provided but no feUID receivedwhen it is not — the fastest way to confirm your token is valid. The token itself is redacted from all SDK logs.
Managing the token lifecycle
Your application owns the token lifecycle. The SDK reads ymAuthenticationToken once, during init(), and does not run a refresh timer of its own — so renewal stays in one place alongside the rest of your auth stack, and you keep whatever expiry policy your identity provider already enforces.
The recommended approach:
- Track expiry in your app. The value you pass is the encrypted token returned by the
encryptYmAuthAPI, so it cannot be inspected in the browser. Start a timer when you obtain the token, based on the session length configured for your AI agent (ymAuthSessionDurationInSec, 30 minutes by default). Having your backend return that expiry alongside the token keeps the two in step. - Renew ahead of time, on your own schedule.
- Apply the new token by creating a fresh SDK instance (below).
- Keep a safety net in case a token lapses mid-session (below).
Applying a renewed token
Create a new instance with the new token and reconnect:
async function applyNewToken(newToken: string) {
sdk.disconnect();
sdk = new YellowChat();
registerEventHandlers(sdk); // re-subscribe on the new instance
await sdk.init({ bot: "your-bot-id", ymAuthenticationToken: newToken });
await sdk.connect(); // identity resolves from the new token's feUID
}
Event subscriptions belong to the instance they were registered on. Keep your
sdk.on(...)calls in a single function, as above, so re-subscribing after renewal is one line.
Detecting a lapsed token mid-session
If a token lapses while a session is open, message sends are rejected and reported through message:failed. The HTTP status is on error.details:
sdk.on("message:failed", async ({ error }) => {
if (error.code === "API_ERROR" && error.details?.status === 401) {
await applyNewToken(await fetchFreshToken());
}
});
The WebSocket stays connected in this case — the token is checked by the messaging API rather than on the socket — so
isConnected()still returnstrue. Use the handler above rather than connection state to detect a lapsed token.
Coming from the embedded chat widget
The YM authentication page describes a ym-revalidate-token event and window.YellowMessengerPlugin.revalidateToken(). Those belong to the embedded chat widget, which manages its own token timer through the YellowMessengerPlugin global. The Headless SDK is installed as a package and has no such global, so you drive the same outcome explicitly:
| Embedded widget (YM auth) | Headless SDK |
|---|---|
ymAuthSessionDurationInSec sets the session length | The same setting still applies — your app schedules renewal against it |
ym-revalidate-token event signals renewal is due | Your own expiry timer, plus the 401 handler above |
window.YellowMessengerPlugin.revalidateToken(token) | applyNewToken(token) — new instance, init(), connect() |
Reserved event names
auth:required, auth:verified, auth:failed and auth:tokenRefreshNeeded appear in the SDK's TypeScript definitions and are reserved for future use — they are not emitted in the current release. Use the token lifecycle pattern above instead of subscribing to them.
The same applies to the AUTH_TOKEN_EXPIRED and AUTH_TOKEN_INVALID error codes. A rejected token is reported as API_ERROR, with the HTTP status available on error.details.status, as shown above.
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.