# Embed the web widget

> Add the K-Agent chat widget to your site with one script tag, and let signed-in users chat as verified customers with client tokens minted on your server.

The widget is a chat bubble for your website: one `<script>` tag, no build step, and it works on any site. It is a Web Component (`<k-agent-chat>`) in its own shadow DOM, so your styles and its styles never clash. It speaks Arabic (right to left) and English, streams replies, shows your team's replies live during a handoff, and stays under 60 KB gzipped.

## 1. Create a publishable key

In the dashboard, open **API keys → Create publishable key** and list the **allowed origins** — the exact sites that may load the widget:

- write each origin as `scheme://host[:port]`, for example `https://www.example.com`;
- `https://*.example.com` matches exactly one subdomain level;
- `http://localhost:3000` must be listed explicitly for local development;
- at least one origin is required (`422 allowed_origins_required`).

A publishable key (`kt_pk_live_…`) is safe to put in your page: it can only read the widget's settings and start widget sessions, and only from those origins.

## 2. Add the script

Paste this before `</body>`:

```html
<script
  src="https://api.k-agent.kerneltics.com/widget/v1.js"
  data-agent="agt_01k6rz1m3w8q4t7v9x2b5c0dnf"
  data-key="kt_pk_live_…"
  defer
></script>
```

That's all for anonymous visitors. The widget loads the agent's **published** settings — `bot_name`, `greeting`, `launcher_label`, `theme.accent` and `theme.position` — so you change its look from the agent editor, not your code.

| Script attribute | Description |
|---|---|
| `data-agent` | The agent's ID. |
| `data-key` | Your publishable key. |
| `data-api` | Optional. The API origin, if it differs from the origin that serves the script. |

### Or place the element yourself

To control where and how the chat appears, load the script without `data-agent` and add the element:

```html
<script src="https://api.k-agent.kerneltics.com/widget/v1.js" defer></script>

<k-agent-chat
  agent="agt_01k6rz1m3w8q4t7v9x2b5c0dnf"
  publishable-key="kt_pk_live_…"
  lang="ar"
  position="end"
></k-agent-chat>
```

| Element attribute | Description |
|---|---|
| `agent` | The agent's ID. Required. |
| `publishable-key` | Your publishable key. |
| `token-endpoint` | A URL on your site that returns a client token for the signed-in user (step 3). |
| `api-base` | Optional API origin. |
| `lang` | `ar` or `en`. Defaults to the element's `lang`, then `<html lang>`, then the agent's language. Arabic is laid out right to left. |
| `position` | `start` or `end` (default). Follows the page direction: `end` is bottom-right in English and bottom-left in Arabic. |
| `open` | Present to start with the chat open. |

## What visitors get

- Replies stream in as they are written; messages from visitors and agents are shown with the right direction for their language.
- The conversation is kept per browser: a visitor who comes back on the same device continues where they left off. A **New conversation** item starts fresh.
- During a handoff the header shows that a person is now in the chat, and your team's replies appear live.
- The widget is keyboard- and screen-reader-friendly, and goes full screen on phones.

### Anonymous visitors

Visitors who are not signed in are **anonymous end users**. For their safety and yours:

- tools with side effects are off unless the agent sets `tools.allow_anonymous_actions`;
- tools that need a verified identity never run;
- limits apply: per IP, 20 conversations and 60 messages an hour; at most 40 messages per conversation; and `widget.anonymous_daily_conversations` per agent (default 200). Past a limit the visitor sees a polite "try again later".

## 3. Signed-in users (verified)

When the visitor is logged in to your site, let them chat **as themselves**: the agent can then use identity-bound tools ("where is *my* order?"), remember their past actions, and your team sees who they are.

Your server mints a short-lived **client token** for the user with your secret key; the widget fetches it from a `token-endpoint` on your site. The secret key never reaches the browser.

```html
<script src="https://api.k-agent.kerneltics.com/widget/v1.js" defer></script>

<k-agent-chat
  agent="agt_01k6rz1m3w8q4t7v9x2b5c0dnf"
  publishable-key="kt_pk_live_…"
  token-endpoint="/kagent/token"
></k-agent-chat>
```

The widget sends a `POST` to `token-endpoint` from your page (with your site's cookies, as a same-origin request) and expects JSON with `token` and `expires_at`, exactly as `POST /v1/client_tokens` returns them. It asks again before the token expires.

**JavaScript**

```js
// Node.js + Express. `requireLogin` is your own authentication middleware.
import express from 'express';

const app = express();

app.post('/kagent/token', requireLogin, async (req, res) => {
  const r = await fetch('https://api.k-agent.kerneltics.com/v1/client_tokens', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.KAGENT_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      agent: 'agt_01k6rz1m3w8q4t7v9x2b5c0dnf',
      end_user: {
        external_id: req.user.customerId, // your stable ID — never an email or phone
        name: req.user.firstName,
        traits: { plan: req.user.plan },
      },
      ttl_seconds: 900,
    }),
  });
  if (!r.ok) return res.status(502).json({ error: 'token_unavailable' });
  const { token, expires_at } = await r.json();
  res.set('Cache-Control', 'no-store').json({ token, expires_at });
});
```

**Python**

```python
# Flask. `login_required` and `current_user` come from your own auth (e.g. Flask-Login).
import os
import requests
from flask import Flask

app = Flask(__name__)

@app.post("/kagent/token")
@login_required
def kagent_token():
    r = requests.post(
        "https://api.k-agent.kerneltics.com/v1/client_tokens",
        headers={"Authorization": f"Bearer {os.environ['KAGENT_API_KEY']}"},
        json={
            "agent": "agt_01k6rz1m3w8q4t7v9x2b5c0dnf",
            "end_user": {
                "external_id": current_user.customer_id,  # never an email or phone
                "name": current_user.first_name,
                "traits": {"plan": current_user.plan},
            },
            "ttl_seconds": 900,
        },
        timeout=10,
    )
    if not r.ok:
        return {"error": "token_unavailable"}, 502
    data = r.json()
    return {"token": data["token"], "expires_at": data["expires_at"]}, 200, {"Cache-Control": "no-store"}
```

- Protect the endpoint with your normal login, and mint tokens only for the logged-in user. A token lets its holder chat as that person for up to `ttl_seconds` (at most 3600; 900 is a good default).
- Use a stable, opaque `external_id` for the user. If your only key is an email or phone number, [hash it](/docs/en/concepts/sessions/#keep-personal-data-out-of-ids).
- To continue one specific conversation, add `"session": "<sess_ or external_id>"` when minting.
- To pass values the agent declares as `client_settable` variables, add `"variables": {…}`.
- A token stops working when it expires, when you revoke the key that minted it, or when you erase the end user.

## Content Security Policy

If your site sends a CSP header, allow the widget's script and API calls:

```text
script-src 'self' https://api.k-agent.kerneltics.com;
connect-src 'self' https://api.k-agent.kerneltics.com;
```

## Troubleshooting

| Error | Cause and fix |
|---|---|
| `403 origin_not_allowed` | The page's origin isn't on the key's list. Add it exactly, including the port. |
| `403 origin_required` | The request didn't come from a browser page. Publishable keys only work in browsers. |
| `403 principal_not_allowed` | Browser credentials called a route they can't use. Do that call from your server with a secret key. |
| `401 token_expired` | Your token endpoint returned an expired token, or the clock on your server is off. |
| `429 anonymous_limit_reached` | An anonymous visitor hit a limit. Signed-in users with client tokens are not limited this way. |
