---
summary: "Production embed walkthrough for developers \u2014 server-side token issuance,\
  \ custom positioning, authenticated visitors, CSP considerations."
title: 'Advanced: embed a bot on your site'
path: tutorials/embed-on-your-site
status: published
---

> **Developer-facing tutorial.** This page assumes you write code and run a backend. For the no-code path through the ScaiGrid admin UI, see [Build a bot with the admin UI](./build-with-the-admin-ui).

The [quickstart](../quickstart) drops a static token into the page. This tutorial covers the production version: tokens issued per visitor by your backend, custom styling, authenticated visitors, and how to set up a content-security policy that doesn't block the widget.

## Architecture

Your server, not your client, talks to ScaiGrid. The widget receives only a short-lived, visitor-scoped token that has no power beyond chatting with the one bot.

```mermaid
sequenceDiagram
    participant V as Visitor
    participant YS as Your server
    participant SG as ScaiGrid

    V->>YS: GET /page
    YS->>SG: POST /embed-token
    SG-->>YS: { token, ttl }
    YS-->>V: HTML + token
    V->>SG: loads widget.js + chats via token
```

## 1. Server-side token issuance

Add an endpoint to your backend that mints a token on demand. Cache nothing — each visitor gets their own.

```python
# Flask-style example
from flask import Flask, request, jsonify
import httpx, os

app = Flask(__name__)

@app.route("/chat-token")
def chat_token():
    visitor = current_visitor()    # your auth — may be anonymous
    resp = httpx.post(
        f"{os.environ['SCAIGRID_HOST']}/v1/modules/scaibot/bots/{BOT_ID}/embed-token",
        headers={"Authorization": f"Bearer {os.environ['SCAIGRID_API_KEY']}"},
        json={
            "ttl_seconds": 3600,
            "visitor_id": visitor.id if visitor else None,
            "visitor_email": visitor.email if visitor else None,
            "metadata": {"plan": visitor.plan} if visitor else None,
        },
    )
    return jsonify(resp.json()["data"])
```

```javascript
// Express equivalent
app.get("/chat-token", async (req, res) => {
  const visitor = req.user; // your auth
  const upstream = await fetch(
    `${process.env.SCAIGRID_HOST}/v1/modules/scaibot/bots/${BOT_ID}/embed-token`,
    {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${process.env.SCAIGRID_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        ttl_seconds: 3600,
        visitor_id: visitor?.id,
        visitor_email: visitor?.email,
      }),
    },
  );
  const { data } = await upstream.json();
  res.json(data);
});
```

The response includes `token` and `expires_at`. Pass the token to the widget; expiry is informational (the widget refreshes itself automatically if you give it your endpoint).

## 2. Loading the widget with auto-refresh

Instead of pasting a static token, point the widget at your token endpoint:

```html
<script
  src="https://scaigrid.scailabs.ai/static/modules/scaibot/widget.js"
  data-bot-id="bot_abc123"
  data-token-url="/chat-token"
  async>
</script>
```

The widget fetches `/chat-token` on load, then again 60 seconds before expiry. If your visitor is anonymous, the widget passes no extra headers; if authenticated, your endpoint reads session cookies as normal.

## 3. Visitor identification

Pass identity attributes so conversations attribute to the right user in analytics:

```html
<script
  src="..."
  data-bot-id="bot_abc123"
  data-token-url="/chat-token"
  data-user-id="usr_42"
  data-user-email="alice@example.com"
  data-user-name="Alice"
  async>
</script>
```

The token issuance pattern is better — `data-user-*` attributes are advisory and could be spoofed by a sufficiently determined visitor. Critical attribution should come from the server-issued token, not from HTML attributes.

## 4. Customising appearance

Built-in attributes:

```html
<script
  src="..."
  data-bot-id="..."
  data-token-url="/chat-token"
  data-position="bottom-right"
  data-primary-color="#4F46E5"
  data-launcher-icon="speech-bubble"
  data-open-on-load="false"
  data-locale="en"
  async>
</script>
```

For custom CSS:

```html
<link rel="stylesheet" href="/your-scaibot.css">
```

```css
:root {
  --scaibot-primary: #4F46E5;
  --scaibot-bg: #ffffff;
  --scaibot-text: #1a1a1a;
  --scaibot-font: "Inter", sans-serif;
  --scaibot-border-radius: 12px;
}
.scaibot-launcher { box-shadow: 0 10px 24px rgba(0,0,0,.18); }
```

Every variable is documented in the widget reference (under `Customisation` in the admin UI).

## 5. Embedded-in-page layout

For a chat panel that lives inside a layout (instead of a floating bubble), mount the widget into a container:

```html
<div id="my-chat" style="height: 600px;"></div>
<script
  src="..."
  data-bot-id="..."
  data-token-url="/chat-token"
  data-mount="#my-chat"
  data-mode="inline"
  async>
</script>
```

## 6. CSP

If you serve under a strict content-security policy, add:

```
script-src 'self' https://scaigrid.scailabs.ai;
connect-src 'self' https://scaigrid.scailabs.ai;
img-src    'self' https://scaigrid.scailabs.ai data:;
style-src  'self' 'unsafe-inline';      # the widget injects styles
font-src   'self' data:;
```

The widget streams responses over Server-Sent Events; `connect-src` must allow the ScaiGrid host.

## 7. Cleaning up across SPAs

If your site is a single-page app, the widget persists across route changes by default. To unmount it (e.g. on logout):

```javascript
window.ScaiBot?.destroy();
```

To remount with a new token:

```javascript
window.ScaiBot?.init({ token: newToken, botId: "bot_abc123" });
```

## Common gotchas

- **403 on `/chat`** — token expired and your `data-token-url` is unreachable. Check your endpoint returns 200 with a fresh token.
- **Widget visible but never connects** — usually CSP blocking `connect-src` to ScaiGrid. Open the browser console; the widget logs a clear error.
- **Conversations not attributing to users** — `data-user-id` was set in HTML but token didn't carry it. Mint tokens with `visitor_id` from your server.
- **Widget on top of your nav bar** — set `data-z-index="10"` (default is 9999, deliberately high). Or use `data-mount` with inline mode.
