Skip to main content
← All docs
UUsable
    usable/chat

    Chat Embeds & Widget Integration

    Updated 2026-07-08

    |Open in||History

    Chat embeds and widget integration

    Release: v1.173.0

    A Usable Chat embed is an iframe chat experience mounted on a host page. It is configured in Usable Chat, protected by an embed key, and optionally controlled by a TypeScript SDK that communicates with the iframe via postMessage.

    Quickstart: basic iframe

    1. In Usable Chat, open Settings → Embeds.
    2. Create an embed configuration.
    3. Create an embed key with allowed domain(s), for example https://docs.example.com.
    4. Copy the key token and paste the iframe snippet:
    <iframe
      id="usable-chat"
      src="https://YOUR_USABLE_HOST/embed?EMBED_KEY_QUERY"
      style="width: 420px; height: 640px; border: 0; border-radius: 12px;"
      allow="clipboard-write; file-system"
    ></iframe>

    Use exact production hostnames. Wildcard subdomain patterns such as *.example.com are supported.

    Quickstart: SDK-controlled widget

    The SDK is distributed as a generated file: copy it from the Integrate dialog into your application.

    import { createUsableChatEmbed } from './usable-chat-embed-sdk'
     
    const iframe = document.querySelector<HTMLIFrameElement>('#usable-chat')!
     
    const chat = createUsableChatEmbed(iframe, {
      iframeOrigin: 'https://YOUR_USABLE_HOST',
      onError: (code, message) => console.error('[usable-chat]', code, message),
      onConversationChange: (conversationId) => {
        if (conversationId) localStorage.setItem('lastUsableConversation', conversationId)
      },
    })
     
    chat.onReady(async () => {
      chat.setAuth(await getFreshUserJwt())
      chat.setCssVariables({
        '--uc-primary': '#1f7a5b',
        '--uc-background': '#ffffff',
      })
    })

    Authentication model

    Embed key

    Every embed uses a key token (uc_...) that enforces config status, key status, allowed domains, and optional configured Usable PAT for anonymous operation.

    User bearer token

    Pass a signed-in user token with chat.setAuth(token). Implement onTokenRefreshRequest to provide a fresh token when the iframe requests one:

    const chat = createUsableChatEmbed(iframe, {
      onTokenRefreshRequest: async () => {
        const session = await refreshHostSession()
        return session?.accessToken ?? null
      },
    })

    When an embed chat request returns 401, the iframe asks the parent for a fresh bearer token and retries once if a different token is returned.

    PAT-backed anonymous embed

    If no bearer JWT is supplied and the embed key has a configured Usable PAT, the embed runs as anonymous/stateless.

    Embed configuration

    Key configData fields:

    {
      "ui": {
        "inputMode": "contained",
        "embedded": true,
        "whitelabel": true,
        "showConversationPanel": false,
        "theme": { "mode": "auto", "primaryColor": "#1f7a5b" },
        "ui": {
          "placeholder": "Ask the docs…",
          "quickReplies": ["How do I get started?"],
          "welcomeMessageEnabled": true,
          "welcomeMessage": "Hi — ask me about this documentation."
        },
        "visibility": {
          "titleBar": false,
          "messageFeedback": true,
          "sourceCitations": true,
          "toolCallDisplay": true
        }
      },
      "models": { "locked": true, "default": "anthropic/claude-haiku-4.5" },
      "features": {
        "webSearch": { "enabled": false },
        "fileUploads": { "enabled": true },
        "visualizations": { "enabled": true },
        "fullscreenView": { "enabled": true }
      },
      "experts": {
        "custom": { "enabled": [], "locked": true }
      },
      "conversation": { "stateless": false },
      "defaultContext": { "workspaces": ["workspace-uuid"] },
      "feedback": { "mode": "host-event" }
    }

    SDK methods

    MethodPurpose
    onReady(cb)Run code after the iframe sends READY.
    setAuth(token)Supply/refresh bearer auth.
    addContext(items)Add workspace/fragment/collection context.
    registerTools(tools)Register parent-provided tool schemas.
    setCssVariables(vars)Runtime theme override (only --uc-* keys).
    submitPrompt(text, { submit })Draft or submit text.
    addImage(base64, opts)Add an image; submit: true sends immediately.
    setVisibility(partial)Change whitelabel UI visibility toggles.
    `setTheme('light''dark'
    restoreConversation(id) / newConversation()Conversation lifecycle.
    toggle(visible)Show/hide the embed.
    destroy()Remove listeners and reject pending RPCs.

    Parent tools

    Parent tools let the assistant call host-page business functions:

    chat.onReady(() => {
      chat.registerTools([
        {
          name: 'lookup_order',
          description: 'Look up an order by order number.',
          timeoutMs: 20000,
          parameters: {
            type: 'object',
            properties: { orderNumber: { type: 'string' } },
            required: ['orderNumber'],
          },
        },
      ])
    })
     
    const chat = createUsableChatEmbed(iframe, {
      onToolCall: async (tool, args) => {
        if (tool === 'lookup_order') return await lookupOrder(args)
        throw new Error(`Unknown tool: ${tool}`)
      },
    })

    The expert/embed must allow parent tools by name.

    Startup contract

    Use parentContract when startup blocking is needed (optional, off by default):

    {
      "parentContract": {
        "version": 1,
        "enforce": true,
        "auth": "bearer-with-refresh",
        "requiredTools": ["lookupAccount"],
        "startupTimeoutMs": 5000
      }
    }

    When unsatisfied, the composer is disabled and EMBED_ERROR is emitted with code: "PARENT_CONTRACT_UNSATISFIED".

    Stateless parent-side persistence

    For embeds where conversation storage lives in the host application:

    { "conversation": { "stateless": true } }

    The iframe emits CONVERSATION_CREATED and MESSAGE_CREATED events. The parent can restore history with chat.upsertConversationMessages({ conversationId, messages, mode }).

    Feedback

    Enable with visibility.messageFeedback: true and configure feedback.mode:

    • host-event: iframe emits { type: 'EVENT', payload: { event: 'FEEDBACK', data: { rating, messageId, comment } } }.
    • usable-fragment: iframe posts to /api/v2/embed/feedback?token=... and saves a fragment.

    File and data access

    Opt-in features for trusted host origins:

    {
      "features": {
        "attachments": { "exposeToParent": true },
        "workspaceFiles": { "exposeToParent": true },
        "dataStaging": { "exposeToParent": true, "acceptExtensions": [".gpx"] }
      }
    }

    Then use SDK helpers: chat.attachments.list(), chat.attachments.readBytes(fileId), chat.workspaceFiles.readBytes(fileId), chat.workspaceFiles.putBytes({ fileName, mimeType, base64 }), chat.data.*.

    Theming

    • Use ui.whitelabel: true to strip Usable-branded chrome.
    • Prefer theme.mode: 'auto' for device-following light/dark behavior.
    • Use setCssVariables for runtime changes (only --uc-* keys are accepted).
    • Embed theme is stored under localStorage["uc-embed-theme"], isolated from the dashboard.

    Troubleshooting

    IssueSolution
    Auth/domain errorConfirm the key is active, host origin matches allowedDomains, and setAuth runs after READY.
    SDK events never fireVerify iframeOrigin matches the Usable host exactly; confirm the page does not sandbox the iframe.
    Parent tools are ignoredRegister tools after READY; ensure the expert/embed config allows parent tools by name.
    Feedback does not saveConfirm visibility.messageFeedback: true and that feedback.mode matches the desired destination.
    Link clicks do nothingVerify the copied SDK includes OPEN_LINK handling and the URL is http: or https:.