> ## Documentation Index
> Fetch the complete documentation index at: https://docs.spinnable.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Website Chat Widget

> Embed an AI-powered chat widget on your website so visitors can interact with your Spinnable worker directly.

## Overview

The Website Chat Widget lets you embed a Spinnable worker directly on your website as a floating chat window. Visitors can ask questions, get support, or interact with your worker — all without leaving your site.

<Info>
  The widget is configured from your worker's **Settings → Website Widget** section in the Spinnable dashboard. This guide covers the implementation details for embedding and customizing the widget on your website.
</Info>

***

## Quick Start

<Steps>
  <Step title="Enable the widget">
    In your worker's dashboard, go to **Settings → Website Widget**, create a widget key, and configure your allowed domains.
  </Step>

  <Step title="Add the embed code">
    Copy the embed snippet from the dashboard and paste it into your website's HTML, just before the closing `</body>` tag:

    ```html theme={null}
    <script
      src="https://app.spinnable.com/widget/v1/embed.js"
      data-api-url="https://api.spinnable.com"
      defer
    ></script>
    <spinnable-widget key="wk_xxxxx"></spinnable-widget>
    ```

    Replace `wk_xxxxx` with the widget key from your dashboard.
  </Step>

  <Step title="You're live!">
    The widget appears as a floating chat button in the bottom-right corner of your page. Visitors can click it to start chatting with your worker.
  </Step>
</Steps>

<Warning>
  Make sure your website's domain is added to the **Allowed Domains** list in the widget settings. The widget will not work on domains that aren't explicitly allowed.
</Warning>

***

## Visitor Identification

By default, the widget assigns each browser a random anonymous ID stored in `localStorage`. This means visitors get a fresh identity per device/browser and their sessions are not linked across devices.

To provide a better experience — like linking conversations to your own user accounts and letting visitors see their past sessions — you can identify visitors using `SpinnableWidget.init()`.

<Tabs>
  <Tab title="Anonymous (Default)">
    When no visitor identification is provided, the widget automatically:

    * Generates a unique ID per browser and stores it in `localStorage`
    * Sessions are tied to that browser only
    * If the visitor clears their browser data, their history is lost

    **No code changes needed** — this is the default behavior.
  </Tab>

  <Tab title="Identified Visitor">
    To tie conversations to your own user system, call `SpinnableWidget.init()` with a `visitorId`:

    ```html theme={null}
    <script>
      window.SpinnableWidget = window.SpinnableWidget || {};
      window.SpinnableWidget.init = window.SpinnableWidget.init || function(opts) {
        window._spinnableWidgetInit = opts;
      };
    </script>

    <script
      src="https://app.spinnable.com/widget/v1/embed.js"
      data-api-url="https://api.spinnable.com"
      defer
    ></script>
    <spinnable-widget key="wk_xxxxx"></spinnable-widget>

    <script>
      SpinnableWidget.init({
        visitorId: "user-12345"
      });
    </script>
    ```

    When you provide a `visitorId`:

    * **Sessions persist across devices** — the same visitor sees their full conversation history wherever they log in
    * **Conversations are linked** to your internal user ID
    * The visitor ID can be any non-empty string — all values are normalized to a deterministic UUID scoped to your worker, so the same ID on different workers yields different internal IDs

    <Tip>
      Use the same user ID from your own authentication system as the `visitorId`. This makes it easy to link widget conversations with your existing user records.
    </Tip>
  </Tab>

  <Tab title="Identified + Profile">
    You can optionally include profile information that will appear in Spinnable's **Oversight** panel and be available to your worker:

    ```html theme={null}
    <script>
      window.SpinnableWidget = window.SpinnableWidget || {};
      window.SpinnableWidget.init = window.SpinnableWidget.init || function(opts) {
        window._spinnableWidgetInit = opts;
      };
    </script>

    <script
      src="https://app.spinnable.com/widget/v1/embed.js"
      data-api-url="https://api.spinnable.com"
      defer
    ></script>
    <spinnable-widget key="wk_xxxxx"></spinnable-widget>

    <script>
      SpinnableWidget.init({
        visitorId: "user-12345",
        visitor: {
          name: "Jane Smith",
          email: "jane@example.com",
          phone_number: "+1234567890"
        }
      });
    </script>
    ```

    | Field          | Type   | Description             |
    | -------------- | ------ | ----------------------- |
    | `name`         | string | Visitor's display name  |
    | `email`        | string | Visitor's email address |
    | `phone_number` | string | Visitor's phone number  |

    <Note>
      Only `name`, `email`, and `phone_number` are supported. Custom metadata is not passed to the worker.
    </Note>

    <Tip>
      Use the same user ID from your own authentication system as the `visitorId`. This makes it easy to link widget conversations with your existing user records.
    </Tip>

    <Info>
      Profile information is sent with the first message in each session. It helps your worker provide personalized responses and shows visitor details in the Oversight panel.
    </Info>
  </Tab>
</Tabs>

### About the init polyfill

You'll notice the init snippet includes a small "polyfill" block before the widget script:

```html theme={null}
<script>
  window.SpinnableWidget = window.SpinnableWidget || {};
  window.SpinnableWidget.init = window.SpinnableWidget.init || function(opts) {
    window._spinnableWidgetInit = opts;
  };
</script>
```

This ensures `SpinnableWidget.init()` can be called **before** the widget script finishes loading. The widget script loads with `defer`, so this polyfill safely stores your configuration until the widget is ready to pick it up. You can place `SpinnableWidget.init()` anywhere on the page — before or after the widget script.

***

## Session Management

The widget supports multiple conversation sessions per visitor:

* **New visitors** see an intro panel with a welcome message and suggested prompts (configured in the dashboard)
* **Returning visitors** (identified via `visitorId`) see a list of their past sessions and can:
  * Open any previous conversation to continue it
  * Start a new conversation with the **New Chat** button
* The **current session** is persisted in `localStorage` and survives page refreshes

***

## Theming & Customization

### Via the Dashboard

The primary way to customize the widget's appearance is through the **Styling** tab in **Settings → Website Widget**. You can configure:

* **Panel title** — the header text shown at the top of the widget
* **Theme colors** — primary, background, foreground, and text colors
* **Intro panel** — heading, description, and suggested prompt buttons
* **Input placeholder** — the placeholder text in the message input
* **Launcher icon** — choose between `chat`, `help`, or `support` icons
* **Launcher position** — `bottom-right` or `bottom-left`

### Via JavaScript (Per-Page Overrides)

For advanced use cases — like matching different themes on different pages — you can override the dashboard config by passing a `theme` object to `SpinnableWidget.init()`:

```html theme={null}
<script>
  SpinnableWidget.init({
    visitorId: "user-12345",
    theme: {
      primary: "#0066cc",
      primaryForeground: "#ffffff",
      background: "#f8fafc",
      foreground: "#1e293b"
    }
  });
</script>
```

<Info>
  JavaScript overrides take priority over dashboard settings. This lets you use the dashboard as a default while customizing specific pages as needed.
</Info>

<Accordion title="All available theme properties">
  | Property            | Description                                     | Default                        |
  | ------------------- | ----------------------------------------------- | ------------------------------ |
  | `primary`           | Buttons, header, user message bubbles, launcher | Emerald green                  |
  | `primaryForeground` | Text on primary-colored elements                | White                          |
  | `background`        | Widget panel background                         | White                          |
  | `foreground`        | Main text color                                 | Dark gray                      |
  | `border`            | Borders and dividers                            | Light gray                     |
  | `muted`             | Muted UI elements                               | Slate                          |
  | `mutedForeground`   | Secondary text (empty states, descriptions)     | Slate                          |
  | `errorBg`           | Error state background                          | Light red                      |
  | `errorText`         | Error state text                                | Dark red                       |
  | `fontFamily`        | Font stack                                      | `Inter, system-ui, sans-serif` |
  | `borderRadius`      | Panel corner radius                             | `12px`                         |
  | `borderRadiusSm`    | Input and button corner radius                  | `8px`                          |
</Accordion>

***

## Allowed Domains & Security

The widget uses your **Widget Key** and **Origin validation** to ensure only authorized websites can embed your worker:

* Every request from the widget includes the `X-Widget-Key` header and the browser's `Origin` header
* The backend validates both against your configuration
* **Wildcard domains** are supported (e.g., `https://*.acme.com` matches all subdomains)

<Warning>
  If you see a **403 error** in the widget, your website's domain is not in the allowed list. Add it in **Settings → Website Widget → Allowed Domains**.
</Warning>

### Key Regeneration

If you suspect your widget key has been compromised, you can regenerate it from the dashboard:

* A **new key** is issued immediately
* The **old key remains valid for 24 hours** (grace period) so you have time to update your embed code
* After 24 hours, the old key stops working

***

## Action Modes

When creating a widget key, you choose an **action mode** that controls what your worker can do when responding to widget visitors:

| Mode                    | Description                                             |
| ----------------------- | ------------------------------------------------------- |
| **Guest** (recommended) | Read-only interactions — safest for public websites     |
| **Collaborator**        | Limited access — can read and contribute but not manage |
| **Org Member**          | Can read and contribute with broader permissions        |
| **Owner**               | Full access — use with caution on public sites          |

<Warning>
  For public-facing websites, we strongly recommend using **Guest** mode to minimize risk. Only use higher permission levels if your widget is on an authenticated internal tool.
</Warning>

***

## File Uploads

The widget supports file uploads from visitors:

* Maximum **3 files** per message
* Maximum **5 MB** per file
* **Drag-and-drop** is supported

***

## Troubleshooting

<AccordionGroup>
  <Accordion title="Widget shows a 403 error">
    Your website's domain is not in the allowed domains list. Go to **Settings → Website Widget → Allowed Domains** and add your domain (e.g., `https://example.com`). Wildcard patterns like `https://*.example.com` are also supported.
  </Accordion>

  <Accordion title="Widget shows a 401 error">
    The widget key is invalid or has been revoked. Check your key in the dashboard — if it was regenerated more than 24 hours ago, the old key has expired. Update your embed code with the new key.
  </Accordion>

  <Accordion title="Widget doesn't appear on the page">
    Verify the following:

    * The embed code is placed before the closing `</body>` tag
    * The `key` attribute on `<spinnable-widget>` matches your dashboard key
    * The script `src` URL is correct and accessible
    * Check your browser's developer console for errors
  </Accordion>

  <Accordion title="Visitor sessions are not persisting across devices">
    If you're using the default anonymous mode, sessions are tied to `localStorage` — they only exist in that specific browser. For cross-device persistence, implement [Visitor Identification](#identified-visitor) with a consistent `visitorId` from your user system.
  </Accordion>

  <Accordion title="Theme changes aren't showing">
    A few things to check:

    * Dashboard theme changes apply on the next widget load — refresh the page
    * JavaScript overrides via `SpinnableWidget.init()` take priority over dashboard settings
    * Ensure color values are valid CSS (hex, HSL, or named colors)
  </Accordion>
</AccordionGroup>

***

## Full Example

Here's a complete implementation with visitor identification, profile data, and custom theming:

```html theme={null}
<!-- Polyfill: allows SpinnableWidget.init() to be called before the script loads -->
<script>
  window.SpinnableWidget = window.SpinnableWidget || {};
  window.SpinnableWidget.init = window.SpinnableWidget.init || function(opts) {
    window._spinnableWidgetInit = opts;
  };
</script>

<!-- Widget script -->
<script
  src="https://app.spinnable.com/widget/v1/embed.js"
  data-api-url="https://api.spinnable.com"
  defer
></script>

<!-- Widget element -->
<spinnable-widget key="wk_xxxxx"></spinnable-widget>

<!-- Initialize with visitor data and theme -->
<script>
  SpinnableWidget.init({
    visitorId: "user-12345",
    visitor: {
      name: "Jane Smith",
      email: "jane@example.com",
      phone_number: "+1234567890"
    },
    theme: {
      primary: "#6366f1",
      primaryForeground: "#ffffff",
      background: "#ffffff",
      foreground: "#1e293b"
    }
  });
</script>
```

***

## Next Steps

<CardGroup cols={2}>
  <Card title="Communication Channels" icon="messages" href="/concepts/communication-channels">
    Learn about all the ways your workers can communicate
  </Card>

  <Card title="Security Best Practices" icon="shield" href="/guides/security-best-practices">
    Understand worker permissions and security settings
  </Card>
</CardGroup>
