JavaScript SDK
The npm package is the recommended way to integrate ChattyBox from application code. Configure its public key and API URL, then load the maintained floating widget from a persistent browser layout or use the headless methods with your own components.
Version requirements: The published npm SDK is 0.1.4. Its lifecycle and strict NodeNext declaration fixes are available in this release. Full widget cleanup also requires the updated maintained widget.js; self-hosted installations must update that file separately.
Quick Start
bun add @openstaticfish/chattybox
import { Chattybox } from '@openstaticfish/chattybox';
const chattybox = new Chattybox({
apiKey: import.meta.env.PUBLIC_CHATTYBOX_API_KEY,
baseUrl: import.meta.env.PUBLIC_CHATTYBOX_API_URL,
});
const widget = chattybox.mountWidget();
// Keep this loader for the lifetime of the page, including SPA navigation.
Call mountWidget() after document.body exists, not during rendering or server-side execution. It appends a floating widget under document.body, not inside your component. { locale: 'fr' } requests French at initialization only if the project allows script locale overrides.
The import.meta.env.PUBLIC_* names in these examples are illustrative. Use your framework's public configuration convention, such as import.meta.env.VITE_* in Vite or process.env.NEXT_PUBLIC_* in Next.js. In Node.js, read and validate process.env values instead.
Get Your Public Configuration
- Create a project and index your content.
- Test representative questions in the dashboard.
- Open Public Keys and create a browser key. It works from production, previews, staging, and localhost by default.
- Open Embed, select that key, and copy the widget API URL shown with the generated snippet.
Public widget API keys are designed to appear in browser code. They identify a project but are not management credentials. For optional defense-in-depth hardening, use Public Keys > Edit origins to enable a restriction and list the exact browser origins to allow. The package is an ESM client for current Node.js and browser applications that provide fetch.
Restrictions match normalized scheme, hostname, and port, not paths or wildcard subdomains. The API uses the request's Origin, falling back to the origin of Referer. Missing or disallowed origins on a restricted key return 401 Invalid API key, just like an invalid or expired key. Node.js fetch does not automatically send either browser header. Origin checks are not authentication and cannot prevent a non-browser client from forging headers.
Constructor and Exports
new Chattybox(options) takes ChattyboxOptions:
| Option | Type | Default / behavior |
|---|---|---|
apiKey | string | Required, nonblank public widget key. |
baseUrl | string | Required, nonblank string. SDK 0.1.4 requires an absolute HTTP(S) URL without credentials, query string, or fragment. |
fetch | typeof globalThis.fetch | Optional fetch-compatible implementation; otherwise global fetch. |
Copy the HTTP widget API URL from Embed, not the dashboard URL or a Convex .cloud client URL. Trailing slashes are removed, followed by a trailing /chat suffix. Do not include query strings or fragments. The client exposes readonly apiKey and normalized baseUrl properties.
The constructor throws an ordinary Error when its options are missing or not an object, apiKey or baseUrl is missing/non-string/blank, a supplied fetch is not a function, or baseUrl is not an absolute HTTP(S) URL without credentials, a query string, or a fragment.
The package exports Chattybox, ChattyboxError, and the types ChattyboxOptions, SendMessageInput, ChatResponse, WidgetConfig, WidgetTranslations, HostedWidgetOptions, and HostedWidgetHandle. It has no React component or hooks. The ./browser IIFE bundle exposes the two constructors on window.ChattyboxSDK; loading that bundle alone does not mount UI.
Mount the Hosted Widget from Code
Use this when you want ChattyBox's maintained floating interface initialized from application code:
import { Chattybox } from '@openstaticfish/chattybox';
const chattybox = new Chattybox({
apiKey: import.meta.env.PUBLIC_CHATTYBOX_API_KEY,
baseUrl: import.meta.env.PUBLIC_CHATTYBOX_API_URL,
});
const widget = chattybox.mountWidget({ locale: 'fr', debug: false });
mountWidget(options?) returns a HostedWidgetHandle synchronously, before the script has loaded or the UI is ready:
| Field / option | Behavior |
|---|---|
options.scriptUrl | Defaults to https://chattybox.ai/widget.js; override only for a trusted preview or self-hosted loader. |
options.locale | Sets data-locale if nonempty. Project allowLocaleOverride must permit it. |
options.debug | Sets data-debug="true" only when truthy; diagnostics are off by default. |
handle.element | The injected or reused HTMLScriptElement. |
handle.remove() | In SDK 0.1.4, the final matching handle emits the hosted removal signal before removing the script. With hosted widget.js v15 it destroys owned UI/styles, cancels startup work and retries, and aborts in-flight chat. |
With SDK 0.1.4 and hosted widget.js v15, use remove() for component cleanup or consent revocation; window.ChattyBox.destroy() is the hosted equivalent. Cleanup cannot undo a request already processed by the server. Keep the loader in a persistent shell across normal client-side navigation.
SDK 0.1.4 shares a marked script only for an identical mount configuration (public key, normalized API URL, script URL, locale, and debug setting). A changed configuration rejects until every existing handle is removed; it never silently keeps the old options. The SDK does not detect unmarked manual loaders. Choose one integration path per page; do not combine SDK mounting with a plugin, GTM tag, or separate script. The constructor's custom fetch is used only by headless methods, not passed to the hosted widget. There is no SDK ready promise, container target, or reactive locale-update API; hosted v15 provides window.ChattyBox.destroy().
Identical mounts share one widget. Each returned handle owns a reference; call remove() on every handle during cleanup. Removing the last handle cancels pending initialization, requests, and retry timers, then removes the owned widget, styles, font links, script, and global API. Repeated cleanup is safe. A mount with a different key, API URL, script URL, locale, or debug setting throws until all previous handles have been removed. Remove the old mount before switching projects or locales.
The hosted v15 loader retries transport failures, interrupted successful response bodies, in-progress responses, rate limits, and server errors with the same request body and idempotency key. It makes at most 10 attempts total and never schedules an automatic retry beyond 30 seconds; readable Retry-After delays count against that window. After exhaustion, Retry message reuses that logical request; a new question starts a new request. Invalid JSON or an invalid response shape on a successful response is a terminal protocol error, not an automatic retry. Cross-origin API responses must expose Retry-After via Access-Control-Expose-Headers.
Build Your Own UI
Use the headless methods below when your application owns the message list, input, loading and error states, citations, and accessibility.
Send a Message
import { Chattybox } from '@openstaticfish/chattybox';
const chattybox = new Chattybox({
apiKey: import.meta.env.PUBLIC_CHATTYBOX_API_KEY,
baseUrl: import.meta.env.PUBLIC_CHATTYBOX_API_URL,
});
const answer = await chattybox.sendMessage({
message: 'How do I get started?',
});
console.log(answer.message);
console.log(answer.sources);
Set PUBLIC_CHATTYBOX_API_URL to the exact widget API URL from the Embed tab. The SDK accepts either the deployment root or a URL ending in /chat.
The response contains:
| Field | Type | Description |
|---|---|---|
message | string | Answer text or the configured fallback message; render safely as text or sanitized Markdown, not raw HTML. |
conversationId | string | Identifier used to continue this conversation. |
sources | string[] | Source URLs for the answer; may be empty for a fallback. Validate HTTP(S) URLs before rendering links. |
sendMessage(input) returns Promise<ChatResponse>. SendMessageInput accepts only message: string, optional conversationId: string | null, and optional idempotencyKey: string. The SDK sends the public API key in the JSON body. It does not send page context (sourceUrl or sourcePath), a locale, or model options. Validate nonblank input and a maximum trimmed length of 2,000 characters before submitting; the backend enforces this limit, but its internal validation failures currently surface as generic HTTP 500 errors.
sendMessage() does not provide streaming, automatic retries, timeouts, an AbortSignal option, storage, or UI state. A custom fetch implementation can supply transport policy. This no-retry statement applies to headless chat: SDK 0.1.4 retries only getWidgetConfig() once after HTTP 400, and hosted v15 retries its startup GETs. There is no separate wasAnswered or fallback-status field in the public response, so do not invent a structured unsupported-answer result.
Continue a Conversation
Keep the returned conversation ID in your UI state and send it with the next message:
const followUp = await chattybox.sendMessage({
message: 'How do I create a public widget key in the dashboard?',
conversationId: answer.conversationId,
});
Do not reuse one conversation ID across unrelated visitors. Create a new conversation by omitting conversationId for their first message.
Passing null also starts a new conversation. An ID groups stored messages within the project; the current public generation path does not pass previous turns to the model. Make follow-ups self-contained rather than assuming the model remembers the previous answer.
Retry Safety
Supply a unique idempotencyKey for each logical message and retain the exact input for a retry:
const input = {
message: 'Where is the API reference?',
idempotencyKey: crypto.randomUUID(),
};
const result = await chattybox.sendMessage(input);
// If the response is lost, retry the same input, including its original conversationId.
The SDK sends the key as Idempotency-Key but does not create one or retry for you. Keys are project-scoped and must be 1-128 characters matching [A-Za-z0-9][A-Za-z0-9._:-]*. An identical completed request can be replayed; changed input, an in-progress request, or a terminal failure can return 409. Do not blindly retry every 409 or generate a new key after an ambiguous transport failure.
Replay is not permanent: completed and failed records expire 24 hours after reservation, while pending records are not reclaimed on time alone. A retry after terminal-record expiry can execute again. Keep retry windows bounded and investigate persistent conflicts instead of assuming unlimited deduplication.
Handle Errors
import { Chattybox, ChattyboxError } from '@openstaticfish/chattybox';
try {
await chattybox.sendMessage({ message: 'Where is the API reference?' });
} catch (error) {
if (error instanceof ChattyboxError) {
console.error(error.status, error.code, error.message);
} else {
// Network failures and other non-HTTP errors are not wrapped by the SDK.
console.error(error);
}
}
ChattyboxError.status contains the HTTP status. code is present only when the API returns a string code; most current widget API errors do not. Non-2xx responses, a nonempty string error even with HTTP 200, invalid JSON, and non-object JSON throw ChattyboxError. Network/fetch failures pass through unchanged. SDK 0.1.4 also wraps unreadable response bodies as ChattyboxError with RESPONSE_BODY_READ_FAILED. Successful JSON objects are cast to the declared type without field-by-field runtime validation.
Common HTTP outcomes include 400 for malformed input or unsupported translation locales, 401 for key/origin failures, 403 for a disabled project widget (including headless chat), 409 for idempotency conflicts, 413 for chat bodies over 65,536 bytes, 429 for rate limiting, and 500 for internal failures. The SDK does not expose response headers on its result or error.
Headless sendMessage() does not automatically retry. If your UI implements retries, supply one idempotencyKey per logical message and reuse both the key and unchanged input for transport retries. Do not generate a new key for a lost response.
Reuse Project Settings and Translations
The SDK also exposes getWidgetConfig() and getWidgetTranslations(locale). These methods support clients that want to reproduce the hosted widget's project settings and localized labels:
const [config, labels] = await Promise.all([
chattybox.getWidgetConfig(),
chattybox.getWidgetTranslations('en'),
]);
A fully custom UI can ignore them. Keep each visitor's conversationId in that visitor's browser or session state; never share one global conversation ID.
getWidgetConfig() returns Promise<WidgetConfig> and authenticates with Authorization: Bearer. SDK 0.1.4 retries exactly once with an encoded query-string key only after HTTP 400, for intermediaries that strip the header; it does not retry other statuses. WidgetConfig is Record<string, unknown> with only three optional typed properties: localeMode: 'auto' | 'fixed', defaultLocale: string, and allowLocaleOverride: boolean. Narrow other fields before using them. Current server fields and defaults are listed in Hosted widget customization. A successful config request also marks the project as widget-installed, even if your custom client never renders a widget.
getWidgetTranslations(locale) returns Promise<WidgetTranslations> with translations: Record<string, string>, dir: 'ltr' | 'rtl', and the server-selected locale: string. This endpoint is public and does not use the API key or project locale settings. Pass one of en, fr, de, pl, es, it, nl, pt, sv, id, et, fi, cy, or cs. The SDK URL-encodes but does not normalize the locale: fr-CA and FR return 400, unlike the hosted widget's regional-locale normalization. An empty string selects English on the server. All current catalogs are LTR and can be cached for one hour.
For a custom UI, resolve the desired locale yourself, respect allowLocaleOverride and fixed/auto project settings if reproducing hosted behavior, and provide fallbacks for missing translation keys. Locale settings affect UI labels, not the language of a chat request or a guarantee about response quality.
Next Steps
- Prefer a maintained, no-build UI? Install
widget.js. - Customize the hosted UI in Hosted widget customization.
- Before launch, test fallback answers, citations, and mobile behavior with the launch checklist. Test exact origins too if you explicitly enable an origin restriction.