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

# Install the Website Widget

> Drop-in install guide for developers — one script tag, framework recipes, SDK reference, signed visitor identity, CSP notes, verification checklist, and a copy-paste prompt for AI coding agents.

This guide is for the developer embedding the Switchbord chat widget on a website. The install is a single async script tag; everything else on this page is optional depth — SPA recipes, the JavaScript SDK, signed identity for logged-in users, and troubleshooting.

## Prerequisites

From the workspace operator (or **Settings → Integrations → Channels → Website widget** if that's you):

* The **install snippet** (or just the widget **slug** and API base URL).
* Confirmation that your site's origin is in the widget's **allowed origins** list — the exact scheme + host, e.g. `https://www.example.com`. `www` and apex are different origins; add both if both serve the site.

The hosted API base is `https://api.switchbord.ai`. Self-hosted instances substitute their own API domain everywhere it appears below.

## The snippet

Paste this before the closing `</body>` tag on every page that should show the widget:

```html theme={null}
<script async src="https://api.switchbord.ai/widgets/YOUR_WIDGET_SLUG/embed.js"></script>
```

That's the entire install. The script injects a launcher bubble and an isolated iframe; appearance, copy, and AI behavior are all controlled from Switchbord Settings and publish without snippet changes.

<Tip>
  If you want to call the SDK before the script has loaded (for example `setVisitor` in inline code), add the command-queue stub first — calls made before load are queued and replayed:

  ```html theme={null}
  <script>
    window.SwitchbordWidget = window.SwitchbordWidget || { q: [] };
    window.SwitchbordWidget.q.push(["setVisitor", { name: "Ada" }]);
  </script>
  <script async src="https://api.switchbord.ai/widgets/YOUR_WIDGET_SLUG/embed.js"></script>
  ```
</Tip>

## Framework recipes

<AccordionGroup>
  <Accordion title="Plain HTML / any server-rendered site">
    Paste the snippet before `</body>` in your base layout/template so it appears on every page. Done.
  </Accordion>

  <Accordion title="Next.js (App Router)">
    Use `next/script` in the root layout so the widget loads once per navigation session, client-side only:

    ```tsx theme={null}
    // app/layout.tsx
    import Script from "next/script";

    export default function RootLayout({ children }) {
      return (
        <html lang="en">
          <body>
            {children}
            <Script
              src="https://api.switchbord.ai/widgets/YOUR_WIDGET_SLUG/embed.js"
              strategy="lazyOnload"
            />
          </body>
        </html>
      );
    }
    ```

    Do not render the script inside pages that remount on navigation — the loader is idempotent (it won't create a second iframe), but loading it once in the layout is cleaner.
  </Accordion>

  <Accordion title="React (Vite / CRA SPA)">
    Load once in a top-level effect:

    ```tsx theme={null}
    useEffect(() => {
      if (document.getElementById("switchbord-widget-script")) return;
      const s = document.createElement("script");
      s.id = "switchbord-widget-script";
      s.async = true;
      s.src = "https://api.switchbord.ai/widgets/YOUR_WIDGET_SLUG/embed.js";
      document.body.appendChild(s);
    }, []);
    ```
  </Accordion>

  <Accordion title="Google Tag Manager">
    Create a **Custom HTML** tag containing the snippet, trigger it on **All Pages** (or a page-path trigger if you only want it on some pages), and publish. Remember GTM fires from your site's origin, so the origin allowlist requirement is unchanged.
  </Accordion>

  <Accordion title="WordPress">
    Either paste the snippet into your theme's `footer.php` before `</body>`, or use any "insert headers and footers" plugin and place it in the footer section. Site-builder platforms (Webflow, Squarespace, Shopify) all have an equivalent "custom code before `</body>`" setting.
  </Accordion>
</AccordionGroup>

## Content-Security-Policy

If your site ships a CSP, allow the Switchbord API origin in these directives:

```
script-src  https://api.switchbord.ai
frame-src   https://api.switchbord.ai
connect-src https://api.switchbord.ai
```

The widget UI runs inside an iframe served from the API origin, so your page only needs to load the loader script and embed the frame — no inline-script or style allowances are required from your site.

## JavaScript SDK

The loader exposes `window.SwitchbordWidget`:

| Method                                     | Description                                                                      |
| ------------------------------------------ | -------------------------------------------------------------------------------- |
| `open()` / `close()` / `toggle()`          | Control the chat panel.                                                          |
| `show()` / `hide()`                        | Show or hide the launcher and panel entirely (e.g. hide on checkout pages).      |
| `send(text)`                               | Send a message on the visitor's behalf.                                          |
| `setVisitor({ name, email, anonymousId })` | Attach **unverified** visitor profile hints. Never overwrites verified identity. |
| `setAttributes({ ... })`                   | Attach unverified custom attributes to the visitor.                              |
| `login(token, attributes?)`                | Verify the visitor with a signed identity token (see below).                     |
| `logout()`                                 | Drop the verified identity for this session.                                     |
| `on(event, fn)`                            | Subscribe to events. Returns the API for chaining.                               |

Events for `on(event, fn)`:

| Event            | Fires when                                                        |
| ---------------- | ----------------------------------------------------------------- |
| `ready`          | The widget frame has booted.                                      |
| `open` / `close` | Panel state changes.                                              |
| `message`        | The transcript updates (payload includes messages).               |
| `unread`         | The unread count changes while the panel is closed (`{ count }`). |
| `error`          | A widget-level error occurred (`{ message }`).                    |
| `identity`       | An identity action resolved.                                      |

```html theme={null}
<script>
  window.SwitchbordWidget = window.SwitchbordWidget || { q: [] };
  window.SwitchbordWidget.q.push(["on", "ready", () => console.log("chat ready")]);
  window.SwitchbordWidget.q.push(["on", "unread", ({ count }) => console.log(count, "unread")]);
</script>
```

## Signed identity (Secure Mode)

If your site has logged-in users, mint a short-lived HS256 JWT on **your server** and pass it to `login()` so Switchbord verifiably knows who the visitor is. Verified visitors resume their conversation across devices, and operators see verified contact details instead of anonymous sessions.

Setup:

1. In Switchbord, generate the **Widget identity signing secret** under **Settings → Integrations → Provider secrets** (`widget-identity-secret`). It is stored in the workspace Vault; the browser never sees it.
2. Set the widget's identity mode in the widget settings (and optionally **Require identity** to block messaging until login).
3. Mint tokens server-side with these claims:

| Claim                                                      | Value                                                                                                     |
| ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `sub`                                                      | Your user's stable external ID (required)                                                                 |
| `workspace_id`                                             | Your Switchbord workspace ID (required)                                                                   |
| `channel_id`                                               | The widget's channel ID — shown in widget settings (required)                                             |
| `aud`                                                      | The widget slug (or the configured audience)                                                              |
| `iat` / `exp`                                              | Issued-at and expiry. Max TTL 900 seconds by default — mint per page load, do not cache long-lived tokens |
| `iss`                                                      | Optional issuer, must match widget settings if configured                                                 |
| `email`, `name`, `phone`, `locale`, `avatar_url`, `custom` | Optional verified profile fields                                                                          |

<CodeGroup>
  ```ts Node.js theme={null}
  import { createHmac } from "node:crypto";

  const b64url = (v: string | Buffer) => Buffer.from(v).toString("base64url");

  export function mintWidgetToken(user: { id: string; email?: string; name?: string }) {
    const now = Math.floor(Date.now() / 1000);
    const header = b64url(JSON.stringify({ alg: "HS256", typ: "JWT" }));
    const payload = b64url(JSON.stringify({
      sub: user.id,
      workspace_id: process.env.SWITCHBORD_WORKSPACE_ID,
      channel_id: process.env.SWITCHBORD_WIDGET_CHANNEL_ID,
      aud: process.env.SWITCHBORD_WIDGET_SLUG,
      iat: now,
      exp: now + 300,
      email: user.email,
      name: user.name,
    }));
    const signature = createHmac("sha256", process.env.SWITCHBORD_WIDGET_IDENTITY_SECRET!)
      .update(`${header}.${payload}`)
      .digest("base64url");
    return `${header}.${payload}.${signature}`;
  }
  ```

  ```php PHP theme={null}
  function mintWidgetToken(array $user): string {
    $b64 = fn($v) => rtrim(strtr(base64_encode($v), '+/', '-_'), '=');
    $now = time();
    $header = $b64(json_encode(['alg' => 'HS256', 'typ' => 'JWT']));
    $payload = $b64(json_encode([
      'sub' => $user['id'],
      'workspace_id' => getenv('SWITCHBORD_WORKSPACE_ID'),
      'channel_id' => getenv('SWITCHBORD_WIDGET_CHANNEL_ID'),
      'aud' => getenv('SWITCHBORD_WIDGET_SLUG'),
      'iat' => $now,
      'exp' => $now + 300,
      'email' => $user['email'] ?? null,
      'name' => $user['name'] ?? null,
    ]));
    $sig = $b64(hash_hmac('sha256', "$header.$payload",
      getenv('SWITCHBORD_WIDGET_IDENTITY_SECRET'), true));
    return "$header.$payload.$sig";
  }
  ```

  ```python Python theme={null}
  import hmac, hashlib, json, time, base64, os

  def _b64url(data: bytes) -> str:
      return base64.urlsafe_b64encode(data).rstrip(b"=").decode()

  def mint_widget_token(user: dict) -> str:
      now = int(time.time())
      header = _b64url(json.dumps({"alg": "HS256", "typ": "JWT"}).encode())
      payload = _b64url(json.dumps({
          "sub": user["id"],
          "workspace_id": os.environ["SWITCHBORD_WORKSPACE_ID"],
          "channel_id": os.environ["SWITCHBORD_WIDGET_CHANNEL_ID"],
          "aud": os.environ["SWITCHBORD_WIDGET_SLUG"],
          "iat": now,
          "exp": now + 300,
          "email": user.get("email"),
          "name": user.get("name"),
      }).encode())
      sig = _b64url(hmac.new(
          os.environ["SWITCHBORD_WIDGET_IDENTITY_SECRET"].encode(),
          f"{header}.{payload}".encode(), hashlib.sha256).digest())
      return f"{header}.{payload}.{sig}"
  ```
</CodeGroup>

Then, in the page for logged-in users:

```html theme={null}
<script>
  window.SwitchbordWidget = window.SwitchbordWidget || { q: [] };
  window.SwitchbordWidget.q.push(["login", "TOKEN_FROM_YOUR_SERVER"]);
</script>
```

<Warning>
  The signing secret must never reach the browser. Mint tokens in a server endpoint or inject them into the page server-side. Any standard JWT library (jsonwebtoken, firebase/php-jwt, PyJWT) also works — the hand-rolled examples above just avoid a dependency.
</Warning>

## Verify the install

1. Open your site — the launcher bubble appears bottom-right (or bottom-left per theme) within a second or two.
2. `curl https://api.switchbord.ai/api/v1/widgets/YOUR_WIDGET_SLUG/config` returns `200` with the widget config JSON.
3. Send a test message from the widget — it appears in the Switchbord inbox in real time.
4. Reply from the inbox — the visitor sees it (with the operator's name) within a few seconds; if the panel is closed, an unread badge appears on the launcher.
5. If AI mode is **draft** or **auto-send**, the test message produces a GBCA draft (or autonomous reply) after a short debounce.

## Troubleshooting

| Symptom                                                                                          | Cause / fix                                                                                                                                                                        |
| ------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Launcher never appears, config URL returns `404` with `origin_not_allowed` or `widget_not_found` | Your page's origin isn't in the widget's allowed origins, the slug is wrong, or the widget is disabled. Check exact scheme + host, including `www`.                                |
| `429` responses                                                                                  | Workspace rate limit (120 requests/minute across all widget traffic) or platform-level protection. Back off; if legitimate traffic exceeds this, talk to your Switchbord operator. |
| `identity_required` (`403`) when sending                                                         | The widget has **Require identity** on — call `login()` with a signed token before messaging.                                                                                      |
| `identity_secret_missing` (`503`) on login                                                       | The workspace hasn't generated the widget identity signing secret yet.                                                                                                             |
| `identity_signature_invalid` / `identity_*_mismatch`                                             | Token signed with the wrong secret, or `workspace_id` / `channel_id` / `aud` claims don't match this widget.                                                                       |
| Widget loads but messages don't reach the inbox                                                  | Check the browser console for blocked `connect-src` (CSP) or an ad-blocker interfering with the API origin.                                                                        |
| Conversation resets for anonymous visitors                                                       | Expected when browser storage is cleared. Use signed identity for durable threads.                                                                                                 |

## Install prompt for AI coding agents

Give this to Cursor, Claude Code, Copilot, or any coding agent working on your website. Replace the two placeholders, paste, and let it run.

```text theme={null}
Install the Switchbord chat widget on this website.

CONTEXT
- Widget script URL: https://api.switchbord.ai/widgets/YOUR_WIDGET_SLUG/embed.js
  (replace YOUR_WIDGET_SLUG; self-hosted Switchbord replaces the domain too)
- The site's public origin(s) must already be allowlisted in Switchbord
  (Settings -> Integrations -> Channels -> Website widget). Origins in use: YOUR_SITE_ORIGINS
- The widget is a single async script tag. It injects a launcher button and an
  isolated iframe. No npm package, no build step, no API key in the browser.

TASK
1. Detect the site's framework/stack by inspecting the repository.
2. Add the script tag so it loads once on every public-facing page, before </body>:
   <script async src="https://api.switchbord.ai/widgets/YOUR_WIDGET_SLUG/embed.js"></script>
   - Next.js App Router: use next/script with strategy="lazyOnload" in app/layout.tsx.
   - Next.js Pages Router: use next/script in pages/_app or _document (body, not head).
   - Plain HTML / server templates: add to the shared base layout/footer template.
   - React SPA (Vite/CRA): inject once from a top-level useEffect, guarded by an
     element-id check so hot reloads and remounts cannot add it twice.
   - Do NOT add it to admin-only or checkout-critical pages if the repo clearly
     separates those layouts; otherwise site-wide is correct.
3. If the site has a Content-Security-Policy (check meta tags, next.config,
   nginx/vercel headers config), extend it:
   script-src, frame-src, connect-src must each include https://api.switchbord.ai
4. Do not add any other code, wrappers, or configuration. Do not attempt to call
   widget APIs from the server. The optional JS SDK is window.SwitchbordWidget
   (open/close/setVisitor/login) with a pre-load command queue
   window.SwitchbordWidget.q — only wire it if the task explicitly asks.

VERIFY
1. Build/serve the site locally.
2. Confirm exactly one script tag for /widgets/YOUR_WIDGET_SLUG/embed.js is present
   in the rendered HTML of at least two different pages.
3. curl -s -o /dev/null -w "%{http_code}" https://api.switchbord.ai/api/v1/widgets/YOUR_WIDGET_SLUG/config
   should print 200 (404 means wrong slug or origin not yet allowlisted — report it, don't work around it).
4. Report: files changed, where the tag was placed, CSP changes made (if any),
   and the local verification results. Note that the launcher only renders on
   allowlisted origins, so localhost may not show it unless localhost is allowlisted.
```

<Note>
  Localhost testing: add `http://localhost:3000` (or your dev port) to the widget's allowed origins while developing, and remove it before go-live. An empty allowlist allows all origins — fine for a first smoke test, not for production.
</Note>
