SDK & Integration

Control the chat widget from JavaScript

Updated 95 views

The script widget exposes a global, window.KookeeChat, so your own code can open the panel, send a question, or react to the visitor opening it.

The most common use: hide the built-in launcher and open the chat from your own "Need help?" button.

The methods

  • init(config) — start the widget with the given settings.

  • open(target?) — open the panel. With a target it lands on a tab or on one help article: open('home'), open('ask'), open('help'), open('feedback') or open({ article: 'slug' }).

  • close() — minimize the panel, keeping the conversation.

  • toggle() — open if closed, close if open.

  • sendMessage(text) — send a question as if the visitor typed it. In a widget without the Ask AI tab it does nothing and logs a warning, so no answer is generated.

  • stop() — end the answer in progress, keeping the text that already arrived. Same as the stop button.

  • clearMessages() — start a fresh conversation.

  • identify(visitor) — say who is chatting, so the conversation is labelled in your dashboard.

  • reset() — the visitor logged out: discard the conversation and forget who they were.

  • setTheme(theme) — switch to 'light', 'dark' or 'auto' while the widget is running.

  • setLocale(locale?) — switch the widget's language while it is running. Nothing, or 'auto', goes back to following the page.

  • on(event, cb) — listen for 'open', 'close' or 'sourceClick'. Returns a function that unsubscribes.

  • destroy() — remove the widget from the page.

Timing

Two rules keep this from failing silently.

Calls before init() do nothing. They log a warning and return. Five methods are exempt and work at any time: on(), whose early listeners still fire, and identify() / reset() / setTheme() / setLocale(), which set state the widget reads when it mounts — so identifying someone or setting the theme or the language before init() is fine and the order does not matter.

With async on the script tag, add the stub. async means the file may not have run yet when your inline script does, so window.KookeeChat would not exist and the call would throw. A small stub placed before the tag queues every call made in the meantime, and the widget replays them in order the moment it loads:

HTML
<script>
  window.KookeeChat = window.KookeeChat || (function () {
    var stub = { _q: [] };
    ['init', 'open', 'close', 'toggle', 'sendMessage', 'stop', 'clearMessages', 'identify', 'reset', 'setTheme', 'setLocale', 'on', 'destroy']
      .forEach(function (m) { stub[m] = function () { stub._q.push([m, [].slice.call(arguments)]); }; });
    return stub;
  })();
</script>
<script async src="https://kookee.dev/chat/latest.js" data-api-key="YOUR_API_KEY"></script>

<script>
  document.getElementById('help-button').addEventListener('click', () => {
    window.KookeeChat.open();
  });
</script>

With the stub in place you can call the API from anywhere on the page without thinking about load order. The ready-made snippet on the Installation tab of the project's Site widget page in the dashboard includes it. Two details:

  • Before the script loads, the stub's methods return nothing — so on() called that early does not hand back an unsubscribe function. Subscribe after load if you need to unsubscribe later.

  • A queued init() takes precedence over data-* attributes on the tag. Configure with one or the other, not both.

Without the stub, drop async — a plain tag guarantees the global exists for every script after it — or wait for the window load event before calling the API.

Starting it yourself with init()

Auto-init from data-* attributes needs a classic external script tag. It does not work for type="module" scripts, or for tag managers that inline the source. In those cases call init() instead:

HTML
<script src="https://kookee.dev/chat/latest.js"></script>
<script>
  window.KookeeChat.init({
    apiKey: 'YOUR_API_KEY',
    title: 'Help',
    greeting: 'Hi! How can I help?',
    hideLauncher: true,
  });
</script>

To keep async on the tag while calling init() yourself, put the stub from Timing above it — the queued init() runs as soon as the script arrives.

init(config) accepts:

  • apiKey — your project API key. Required.

  • baseUrl — override the API base URL.

  • locale — the widget's language: 'auto', the default, follows the page; a tag such as 'fr' fixes it. setLocale() changes it later.

  • title — the panel header.

  • greeting — message shown before the first question.

  • placeholder — input placeholder text.

  • suggestions — an array of strings shown as quick-start pills.

  • tabs — the sections to show, in order, from 'home', 'ask', 'help' and 'feedback', such as ['home', 'ask', 'help']. The widget shows exactly these: ['home', 'help'] is a widget with no chat, and ['ask'] alone is the plain chat.

  • links — [{ label, url }], shown on Home and opened in a new tab. Only web addresses are shown.

  • labels — replaces any of the widget's own texts, in the language it picked. See Show the widget in your visitors' language.

  • anchors — false stops elements with data-kookee-article from opening articles in the widget. Default true.

  • position — 'bottom-right' or 'bottom-left'.

  • theme — 'light', 'dark' or 'auto': the theme it starts with. setTheme() changes it later.

  • primaryColor — accent color.

  • hideLauncher — hide the built-in launcher button.

  • launcherLabel — text for the dismissible label pill next to the launcher.

  • zIndex — stacking order against your own overlays.

  • offset — { x, y } nudge from the corner.

  • defaultOpen — start with the panel open. Until the settings saved in the dashboard arrive, the panel shows a short loading state; if they fail or take more than five seconds, it shows the plain chat and applies them when they come. Tabs set in code show at once.

  • pageContext — false stops the widget reading the current page. Default true.

  • appContext — an object, or a function returning one, of app state your markup does not expose.

  • visitor — { id, email, name } saying who is chatting, for the dashboard. Same as calling identify().

  • tools — functions the assistant can call in the visitor's browser.

  • builtInTools — { catalog, navigate }, see below.

  • onOpenChange(open) — called whenever the panel opens or closes.

  • onSourceClick(source) — called when the visitor clicks a source chip or an inline citation. Without it, the article opens inside the widget.

  • onNavigate(url) — route client-side instead of a full page load.

title, greeting, placeholder, suggestions, tabs and links can also be saved in the dashboard, on the Customize tab of the Site widget page. A value set here wins over the saved one, key by key. See Add Home and Help tabs to your widget.

Telling it about your app

The widget already reads the page's markup on its own. appContext covers what the markup does not say — the signed-in plan, how many items are in the cart, which experiment a visitor is in:

JavaScript
window.KookeeChat.init({
  apiKey: 'YOUR_API_KEY',
  appContext: () => ({ plan: currentUser.plan, cartItems: cart.length }),
});

Pass a function rather than an object if the values change while the page is open — it is called fresh on every message, so the assistant never works from stale state. An object is fine for values fixed at load.

Treat it like a prompt, not an API: keep it small and human-readable, and do not put secrets in it.

Giving it tools

tools lets the assistant call your own functions, in the visitor's browser, with the visitor's own session. That is how it answers from live data — your prices, their orders — without any of it being copied into Kookee:

JavaScript
window.KookeeChat.init({
  apiKey: 'YOUR_API_KEY',
  tools: [{
    name: 'search_orders',
    description: "Look up the signed-in customer's orders.",
    parameters: { type: 'object', properties: { status: { type: 'string' } } },
    label: 'Checking your orders',
    handler: async ({ status }) =>
      fetch('/api/my/orders?status=' + status, { credentials: 'include' }).then((r) => r.json()),
  }],
});

Only name, description and parameters are sent to Kookee. The handler never leaves your page. See Let the assistant use your own data with tools for the full picture, including what not to do.

Built-in tools

  • catalog — on Shopify and WooCommerce, product search backed by the store's own public API. On by default; builtInTools: { catalog: false } turns it off.

  • navigate — lets the assistant open pages on your site, after asking the visitor. Off by default; builtInTools: { navigate: true } turns it on.

On a single-page app, pair navigate with onNavigate so it routes client-side instead of reloading and losing your app state:

JavaScript
window.KookeeChat.init({
  apiKey: 'YOUR_API_KEY',
  builtInTools: { navigate: true },
  onNavigate: (url) => router.push(url),
});

Your own launcher

Hide the built-in button and drive the widget from your own UI:

HTML
<button id="help-button">Need help?</button>

<script src="https://kookee.dev/chat/latest.js"></script>
<script>
  window.KookeeChat.init({ apiKey: 'YOUR_API_KEY', hideLauncher: true });

  document.getElementById('help-button').addEventListener('click', () => {
    window.KookeeChat.toggle();
  });
</script>

Asking a question for the visitor

Useful for a "Still stuck?" link at the bottom of a page — it opens the chat with the question already asked:

JavaScript
window.KookeeChat.open();
window.KookeeChat.sendMessage('How do I reset my password?');

sendMessage() is ignored while an answer is still streaming, so a double click cannot start two overlapping conversations.

Opening a tab or an article

open() takes a target, so a link in your app can land the visitor exactly where they need to be:

JavaScript
window.KookeeChat.open('help'); // the Help tab
window.KookeeChat.open({ article: 'reset-your-password' }); // one help article, by its slug

A tab the widget does not show opens its first tab instead. For a link in your page you do not need code at all: give the element data-kookee-article="slug" — see Open help articles inside the widget.

Following your site's theme

If your site has its own light/dark toggle, call setTheme() from it so the widget changes with the page:

JavaScript
// inside your own theme toggle: 'light', 'dark' or 'auto'
window.KookeeChat.setTheme(isDark ? 'dark' : 'light');

The widget switches in place: an open panel stays open, and an answer that is still streaming keeps streaming. Set the starting theme with data-theme or theme in init(), then call setTheme() on every change. A "system" choice in your toggle maps to 'auto', which follows the visitor's operating system. A value from setTheme() wins over the configured theme whichever ran first, and destroy() forgets it.

Following your site's language

The widget already follows <html lang>, including when your site changes it without a reload. If your language switch does not touch that attribute, call setLocale() from it:

JavaScript
// inside your own language switch
window.KookeeChat.setLocale('fr');

// back to following the page
window.KookeeChat.setLocale();

Home and Help reload in the new language, an open article closes back to its tab, and the conversation stays. A value from setLocale() wins over data-locale and locale in init(), and destroy() forgets it. See Show the widget in your visitors' language for how the widget picks a language on its own.

Saying who is chatting

identify() labels the conversation in your dashboard with the person having it, so a row reads their name instead of Anonymous and a search for their email finds it:

JavaScript
// after your own login completes
window.KookeeChat.identify({ id: user.id, email: user.email, name: user.name });

// on logout
window.KookeeChat.reset();

All three fields are optional, and the identity is never sent to the model — use appContext for anything the assistant should actually know. It is also never verified, so it must not gate anything.

Identifying a different person, or calling reset(), throws the current conversation away and starts a new session. That is what stops the next person on a shared computer from reading the previous one's chat. See Identify who is chatting for the full rules.

Listening for events

on() returns an unsubscribe function, so tear listeners down when the surrounding component goes away:

JavaScript
const unsubscribe = window.KookeeChat.on('open', () => {
  // e.g. record that support chat was opened
});

// later
unsubscribe();

'sourceClick' fires when the visitor clicks an article — either a chip in the Sources row or an inline citation in the answer text — which is how you route them into your own help center:

JavaScript
window.KookeeChat.on('sourceClick', (source) => {
  window.location.href = '/help/' + source.slug;
});

With neither this listener nor onSourceClick, the click opens the article inside the widget, in a reader with a back button, so the visitor never leaves the page. An article marked chatbot-only has no page of its own and stays inert.

The two are not the same set. Chips are the articles the answer was built on; an answer that lists articles links plenty of them without having read any, and those links still fire this event. source.consulted tells you which kind you received — see How the assistant picks its sources.

Handling routing yourself

The conversation is kept per browser tab and survives page navigation, so a visitor who follows a cited link and comes back finds their chat where they left it. clearMessages() starts over deliberately — the same thing the New chat button in the panel header does.

TypeScript

Type definitions for the globals are published at https://kookee.dev/chat/latest.d.ts. If you install the npm packages instead, the equivalent type is exported:

TypeScript
import type { KookeeChatApi } from '@kookee/react/chat';

Was this article helpful?