Skip to content

Chat Widget SDK

Drop an AI chat assistant, trained on your own content, onto any website with one script tag. Framework-agnostic, dependency-free, and safe for anonymous visitors.

Overview

The Chat Widget SDK embeds a floating chat assistant into any page. In knowledge mode it answers strictly from the sources you trained it on (URLs, workspace files, folders) and refuses to invent answers. It is designed for public sites: the browser never holds a long-lived credential.

BundleSize (gzipped)Includes
thinkflyflow-chat.min.js~10 KBLauncher, chat, theming, events
thinkflyflow-chat-advanced.min.js~16 KBEverything in basic, plus attachments, image paste, voice, quick replies, feedback, unread badge, session persistence and human takeover

Loading the advanced bundle does not enable its features. They are controlled by the widget settings you configure in the dashboard, so you can change behaviour without redeploying your site.

Install & embed

The simplest integration is a single script tag:

HTML
<script src="https://cdn.thinkflyflow.com/chat-widget/thinkflyflow-chat-advanced.min.js" async></script>
<script>
  window.addEventListener('DOMContentLoaded', function () {
    ThinkFlyFlow.init({
      mode: 'knowledge',
      widgetId: '<your-widget-id>',
      apiBaseUrl: 'https://api.laureljar.com/api/v2',
      getToken: async function () {
        // Your backend mints a short-lived visitor token (see Authentication)
        const res = await fetch('/api/widget-token', { method: 'POST' })
        const data = await res.json()
        return { token: data.token, features: data.features, tier: data.tier }
      },
    })
  })
</script>

The SDK can also be imported as ESM if you bundle your site: import { ThinkFlyFlow } from '@thinkflyflow/chat-widget'. A React wrapper is shown in the repository examples.

Where the script lives.We publish the bundles as static files. Serve them from your own domain if you prefer (copy the file into your project's public/ directory and point script.src at it). Serving from your own origin avoids third-party script CSP complications.

Authentication & tokens

A browser embed must never carry a long-lived secret. The SDK takes a short-lived visitor token minted by your backend. The SDK never mints or stores credentials itself.

The flow is three steps:

StepWhoWhat happens
1YouRegister a widget in the dashboard. You receive a public key and a secret key (shown once).
2Your backendExchange the public and secret key for a short-lived visitor token via the mint endpoint.
3The browserThe SDK receives that token and uses it for chat. It expires and is re-minted as needed.
Your backend (Node, Next.js route handler)
// POST /api/widget-token  - runs server-side only
export async function POST() {
  const res = await fetch('https://api.laureljar.com/api/v2/widgets/token/', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      public_key: process.env.WIDGET_PUBLIC_KEY,
      secret_key: process.env.WIDGET_SECRET_KEY,
      origin: 'https://your-site.com',
    }),
  })
  const body = await res.json()
  // Pass through features + tier so dashboard settings drive the widget.
  return Response.json({
    token: body.data.token,
    features: body.data.features,
    tier: body.data.tier,
  })
}

Pass features and tier through. If your endpoint drops them, the widget falls back to defaults and your dashboard toggles will appear to do nothing.

Tokens expire (default one hour). When a request returns 401 the SDK calls getToken() again automatically and retries once, so you never need to manage expiry yourself.

Configuration reference

All options are passed to ThinkFlyFlow.init(config).

OptionTypeDefaultDescription
mode'workspace' | 'knowledge'workspaceknowledge answers only from trained sources and is the mode for public sites.
widgetIdstringundefinedThe widget to talk to. Required for knowledge mode and for attachments, voice and handoff.
apiBaseUrlstringprod APIAPI base URL. Point at the test API while verifying.
getTokenfunctionundefinedReturns a token, or { token, features, tier }. Called before the first message and after a 401.
tokenstring | nullundefinedA token you already hold. Prefer getToken so expiry is handled.
refreshTokenstring | nullundefinedWorkspace mode only: enables one silent POST /auth/refresh/ retry on 401.
projectIdstringundefinedWorkspace mode: scopes chat context to a project.
position'bottom-right' | 'bottom-left'bottom-rightCorner the launcher docks to.
titlestringChat with usHeader title and launcher aria-label.
greetingstringundefinedFirst assistant message shown before the visitor types.
placeholderstringType a message…Input placeholder text.
logoUrlstringbuilt-in markBrand logo shown in the header.
brandUrlstringthinkflyflowWhere the header logo and footer link.
avatarUrlstringundefinedOptional support avatar in the header.
theme{ primaryColor, mode }indigo, lightAccent colour and colour scheme.
brandingBrandingConfigserver settingsLogo, title, greeting, colours and attribution. See Brand customisation.
modelstringplatform defaultModel id for this widget. See Choosing the AI model.
zIndexnumber2147483000Stacking order.
maxTokensnumberundefinedResponse length cap forwarded to the API.
modelstringundefinedOptional model override.
containerHTMLElementdocument.bodyCustom mount point.

Tiers & feature flags

Features are resolved with this priority (highest first): init().features, then the flags returned by getToken(), then tier defaults, then hard defaults (everything off).

Operate features from the dashboard, not the site. Set your tier and per-feature toggles in the widget settings so changes take effect on every embedded page on the next load. Hardcoding features in init() overrides the server and makes the dashboard toggles appear broken.

FlagAdvanced defaultWhat it does
attachmentsonVisitors attach files (images, PDFs, documents).
screenshotonPaste an image from the clipboard (Ctrl/Cmd+V) or via the paste button.
voiceonRecord a voice message (push-to-talk by default).
feedbackonThumbs up/down on assistant replies.
unreadBadgeonBadge on the launcher when a reply arrives while closed.
persistSessiononKeep the transcript across page reloads.
humanTakeoveronVisitor sees "a team member is responding" and agent replies appear live.
contextoffSend prior turns with each message for follow-ups.
quickRepliesoffTappable suggestion chips before the visitor types.
handoffoffA "Talk to a human" form capturing name, email and message.
aiPausedDuringTakeoveroffWhen on, the AI stops replying while a human is handling the chat.
maxUploadMb10Per-file upload cap.
acceptcommon typesAccepted file extensions.

Quick replies and handoff are never on by default, even on the advanced tier. They turn on only when you configure them, so a widget does not accidentally show a handoff form you did not ask for.

Knowledge mode

In knowledge mode the widget answers only from the content you trained it on. Train it in the dashboard by adding URLs, workspace files or folders, then run ingestion.

BehaviourWhat the visitor sees
A question matches trained contentAn answer grounded in that content, with sources available to the owner.
No content matchesAn honest "I do not have information about that yet" rather than a made-up answer.
A question about another widgetNever possible: retrieval is scoped to the widget that was trained.

The widget answers with the visitor token only, so it works for anonymous visitors on third-party sites with no account and no login.

Brand customisation

Owners control the widget's appearance from the dashboard (the widget detail page), so branding can change without editing the embedding site. Settings are delivered to the widget with the visitor token.

SettingEffect
Logo URLImage shown in the widget header. Leave blank for the default mark.
Theme colourAny CSS colour. Applied to the launcher, header and outgoing message bubbles. Pick from the colour wheel or enter a hex value.
Colour schemeLight or dark.
TitleHeader title and launcher accessible label.
GreetingFirst assistant message shown before the visitor types.
Input placeholderPlaceholder text in the message box.
Hide attributionRemoves the "Powered by ThinkFlyFlow" footer for white-label sites.

You can also set these per page through init(), which takes precedence over the dashboard. This is useful for a different logo on a landing page, for example:

init config
ThinkFlyFlow.init({
  mode: 'knowledge',
  widgetId: '<your-widget-id>',
  getToken: mint,
  branding: {
    logoUrl: 'https://your-site.com/logo.png',
    primaryColor: '#0ea5e9',
    mode: 'light',
    hideAttribution: true,
  },
})

Hiding the attribution is available on white-label plans. If it is not enabled for your widget the footer stays, and the rest of your branding still applies.

Choosing the AI model

Each widget can use a different model. Pick one from the dashboard (the widget page) or leave it on the platform default. The catalogue lists each model with a short description of what to expect and a relative cost hint, so you can trade quality against cost per widget.

ReadinessMeaning
Higher costPremium models. Best reasoning, charged at a higher credit rate.
Lower costLighter models. Faster and cheaper, good for high-volume sites.
Platform defaultRecommended. Uses whichever model the platform is currently optimised around.

If a model fails, visitors are still answered. The platform automatically falls back through its provider chain and then a free tier, so an outage never leaves a visitor without a reply. When this happens you are emailed, and the widget page shows a notice with the model that actually served the reply.

Alerts are throttled to one per model per hour so a prolonged outage does not flood your inbox. Only workspace owners and admins can change the model.

per-page override via init()
ThinkFlyFlow.init({
  // ...config
  model: 'deepseek-chat', // optional; omit to use the dashboard setting
})

Theming & position

init config
ThinkFlyFlow.init({
  theme: { primaryColor: '#ce42f5', mode: 'light' }, // or 'dark'
  position: 'bottom-right',
  title: 'Ask Laurel Jar',
  greeting: 'Hi! Ask me anything.',
  logoUrl: '/logo.png',
  brandUrl: 'https://your-site.com',
})

The widget renders inside a shadow DOM, so your page CSS cannot leak in and the widget CSS cannot leak out. It sizes itself with dynamic viewport units and respects safe-area insets, so it does not clip on mobile browsers with collapsing toolbars.

Programmatic API

MethodDescription
ThinkFlyFlow.init(config)Mount the widget. Safe to call again to reconfigure.
.open() / .close() / .toggle()Control the panel.
.sendMessage(text)Send a message as if the visitor typed it.
.setToken(token, refreshToken?)Update the token after your own refresh flow.
.setUser({ id, name, email })Attach visitor metadata (never credentials).
.isOpen()Whether the panel is open.
.destroy()Tear down the widget and remove listeners.

Events

Subscribe with on(event, handler). It returns an unsubscribe function.

events
ThinkFlyFlow.on('ready', () => {})
ThinkFlyFlow.on('open', () => {})
ThinkFlyFlow.on('close', () => {})
ThinkFlyFlow.on('message', ({ role, content }) => {})
ThinkFlyFlow.on('error', ({ message, status, code }) => {})

Human handoff & takeover

When handoff is enabled, a visitor can ask for a human. Their name, email, message and the conversation transcript are recorded so nothing is lost even if email delivery fails. Requests appear in your widget inbox.

You can also take over any conversation without the visitor asking. While a team member is handling it, the visitor sees that a team member is responding and your replies appear in their chat. Whether the AI keeps answering during takeover is a per-widget setting (aiPausedDuringTakeover); a human is never the default responder.

Replies are delivered by short polling, which keeps the integration simple and works through restrictive networks. The transport is isolated inside the SDK, so it can be upgraded without changing your integration.

Security model

GuaranteeHow it holds
No secret in the browserOnly a short-lived, expiring visitor token reaches the page. It is scoped to one workspace.
Origin allow-listThe mint endpoint rejects origins not on the widget allow-list. Entries are an exact match, so add every domain you embed on, including www and preview domains.
Cross-tenant isolationRetrieval is scoped to the widget, so one customer content can never surface in another widget.
Safe renderingModel output is escaped before any formatting is applied, so a reply cannot inject markup into your page.
Shadow DOM isolationYour styles and the widget styles cannot affect each other.

Troubleshooting

SymptomLikely causeFix
Widget shows "No widget token available"The mint route failed or the origin is not allowed.Check the mint response status. If 403, add the page origin to the widget allow-list.
Dashboard toggles do nothingfeatures or tier are hardcoded in init(), or your token route drops them.Remove from init() and pass features and tier through from the mint response.
A feature renders that you uncheckedThe saved settings do not include an explicit value for that flag, so a tier default filled in.Save the feature set with an explicit value for every flag.
Handoff form appears unexpectedlyhandoff is enabled in settings or init().Set handoff to false; it is off by default.
Replies never arrive for a visitorThe visitor token expired and was not re-minted.Ensure getToken() is provided so the SDK can re-mint on 401.
CORS or CSP errorsYour Content-Security-Policy blocks the script or API origin.Allow the script origin and connect-src for the API base URL.