Skip to main content

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:

  1. Log in to cloud.yellow.ai.
  2. Go to Extensions → Chat Widget → Deploy.
  3. Copy the host value 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:

SourceUse case
yellowmessengerDefault. Fully headless, SDK-only integrations where the SDK handles all communication (send + receive) over the widget/WebSocket channel.
syncApiHybrid 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 as sender in 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 syncApi for 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 in syncApi mode, agent → user messages are not guaranteed to reach the SDK. They arrive only if (a) the same user id is used for the sync sender and sdk.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, prefer yellowmessenger.

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: true while integrating. The SDK logs feUID received from server when the token is accepted, and ymAuthenticationToken provided but no feUID received when 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:

  1. Track expiry in your app. The value you pass is the encrypted token returned by the encryptYmAuth API, 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.
  2. Renew ahead of time, on your own schedule.
  3. Apply the new token by creating a fresh SDK instance (below).
  4. 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 returns true. 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 lengthThe same setting still applies — your app schedules renewal against it
ym-revalidate-token event signals renewal is dueYour 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 a Content-Type header manually (the SDK does this). Failures throw a YellowChatError with code FILE_UPLOAD_FAILED or FILE_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​

EventPayloadDescription
connection:connecting–Connection attempt started
connection:connected{ userId }Successfully connected
connection:disconnectedDisconnectReasonDisconnected
connection:reconnecting{ attempt, delay }Attempting to reconnect
connection:reconnected{ userId }Successfully reconnected
connection:reconnectFailed–All reconnection attempts failed
connection:errorSDKErrorConnection error

Message events​

EventPayloadDescription
message:receivedIncomingMessageNew message received
message:sent{ id, message }Message sent successfully
message:failed{ messageId, error }Message send failed
message:typingbooleanTyping indicator status

Agent events​

EventPayloadDescription
agent:assignedAgentProfileLive agent assigned to the conversation
agent:left–Agent left or ticket closed/resolved
agent:typingbooleanAgent is typing

Ticket events​

EventPayloadDescription
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​

EventPayloadDescription
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​

EventPayloadDescription
history:previousMessagesIncomingMessage[]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​

  1. Subscribe to events before connect() so you don't miss early messages.
  2. Handle reconnection via connection:reconnecting / connection:reconnected — the SDK auto-reconnects and re-fetches missed history.
  3. Check isConnected() before sending.
  4. Clean up on unmount: call the unsubscribe functions returned by sdk.on(...) and sdk.disconnect().

Troubleshooting​

  • Connection fails immediately — verify the bot id, that host matches 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.