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

# Web chat widget

> Embed Clarity's support chat on your website, open it from your own buttons, and identify signed-in visitors.

The web chat widget adds a chat launcher to your site. Visitors talk to your AI agent and your team, and every conversation arrives in your Clarity inbox as a ticket.

## Install

**1. Add a web integration**

In the cabinet, go to **Settings**, open **Live chat** under **Channels** and click **Add Integration**. Enter the **Domain Address** of the site that will host the widget, without `https://` and without a path, for example `www.example.com`. Leave the widget on **Live chat** and click **Add web integration**.

**2. Copy the inject code**

The **Integration info** dialog shows your **Integration Key**, your **Organization Id** and an **Inject code** snippet with both filled in. Copy the snippet.

**3. Paste it into your page**

Add the snippet to the `<head>` of every page where the chat should appear. The launcher shows up once the page has loaded.

The snippet looks like this:

```html theme={null}
<script id="anecdote-root-script">
  (function() {
    var o = {
      "integrationKey": "YOUR_INTEGRATION_KEY",
      "organizationId": "YOUR_ORGANIZATION_ID",
      "mode": "live_chat",
      "widgetType": "live_chat"
    };
    var s = document.createElement('script');
    s.type = 'text/javascript';
    s.src = "https://live-chat.leap.onclarity.com/inject/inject.js?t=" + Date.now();
    s.async = true;

    s.onload = function() {
      var f = window.anecdoteLiveChatInit;
      if (typeof f === "function") {
        f(o, window.onAnecdoteSuccess, window.onAnecdoteError);
      }
    };

    function add() {
      var t = document.body || document.head || document.documentElement;
      if (t) t.appendChild(s);
    }
    if (document.readyState === 'loading') {
      document.addEventListener('DOMContentLoaded', add);
    } else {
      add();
    }
  })()
</script>
```

> [!NOTE]
>
> The widget only starts on the domain you registered. Clarity checks the page's origin against the channel's **Domain Address**, so a staging site or a second domain needs its own web integration.

## Configuration options

Add options to the `o` object in the snippet. All of them are optional except the two ids.

| Option | Type | Description |
| - | - | - |
| `organizationId` | string | Your organization id. Filled in by the cabinet. |
| `integrationKey` | string | The web channel's integration key. Filled in by the cabinet. |
| `language` | string | UI language of the widget, for example `"en"` or `"ar"`. |
| `placement` | `"left"` or `"right"` | Side of the page the launcher sits on. Default `"left"`. |
| `externalId` | string | Your stable id for the signed-in visitor. |
| `firstName`, `lastName` | string | Visitor's name. |
| `email`, `phone` | string | Visitor's contact details. |
| `avatar` | string | URL of the visitor's picture. |
| `customAttributes` | object | Attributes to set on the contact. See [Custom attributes](#custom-attributes). |
| `userJwt` | string | A signed identity token from your backend. See [Verify the visitor](#verify-the-visitor-signed-identity). |

### Callbacks

The snippet passes `window.onAnecdoteSuccess` and `window.onAnecdoteError` to the widget. Define them before the snippet runs to learn whether the chat session started:

```html theme={null}
<script>
  window.onAnecdoteSuccess = function (payload) {
    // the chat session started
  };
  window.onAnecdoteError = function (error) {
    // the session was refused; error.code says why when identity verification failed
  };
</script>
```

## Open and close the chat from your page

The widget has its own launcher. You can also open it from your own **Chat with us** button, a help link or after a form is submitted. Use whichever form your site can reach:

```js JavaScript theme={null}
window.anecdoteLiveChatOpen();
window.anecdoteLiveChatClose();
window.anecdoteLiveChatToggle();
window.anecdoteLiveChatIsOpen(); // true or false
```

```html HTML attribute theme={null}
<button data-anecdote-live-chat="open">Chat with us</button>
```

```js postMessage theme={null}
window.postMessage({ source: 'anecdote-live-chat', action: 'open' }, '*');
window.postMessage('anecdote-live-chat:close', '*');
```

```js DOM event theme={null}
window.dispatchEvent(new CustomEvent('anecdote-live-chat:toggle'));
```

The functions exist only once the widget script has loaded, so call them with `?.()` from your own scripts or wait for `onAnecdoteSuccess`. A command sent before the widget is ready is applied as soon as it is.

To follow the panel's state, listen for the events the widget posts on every open and close:

```js theme={null}
window.addEventListener('message', (event) => {
  if (event.data?.event === '@anec/widget-opened') { /* opened */ }
  if (event.data?.event === '@anec/widget-closed') { /* closed */ }
});
```

> [!NOTE]
>
> These commands work while **Host page can open/close the widget** is on in the integration dialog. Turn it off and the widget ignores them; its own launcher keeps working.

## Identify signed-in visitors

Pass the visitor's details in the snippet's `o` object so your team sees who they are talking to:

```js theme={null}
var o = {
  "integrationKey": "YOUR_INTEGRATION_KEY",
  "organizationId": "YOUR_ORGANIZATION_ID",
  "mode": "live_chat",
  "widgetType": "live_chat",
  "externalId": "user-42",
  "firstName": "Ada",
  "email": "ada@example.com"
};
```

Values passed this way are a claim the page makes about itself. Anyone can edit page code, so Clarity never treats them as proof of identity. To prove who the visitor is, add a signed token.

### Verify the visitor (signed identity)

Your backend signs a short-lived JWT for the logged-in visitor with the channel's **identity secret**. The widget carries the token to Clarity, which checks the signature and ties the conversation to that user. When the token is valid, its claims take precedence over `externalId` and the other identity options.

**1. Get the identity secret**

Open the web integration in **Settings > Live chat**. Under **Identity verification**, click **Reveal secret** and then **Copy secret**. Store it in your backend's configuration.

**2. Mint a token on your backend**

Sign a JWT with **HS256**, using the secret exactly as the cabinet shows it. Do not decode it first.

| Claim | Required | Rules |
| - | - | - |
| `user_id` | Yes | String up to 255 characters. Your stable id for the user. |
| `exp` | Yes | Expiry in seconds since the Unix epoch, at most 24 hours ahead. Keep it short, for example 10 minutes. |
| `email` | No | Only an address your system has verified. |
| `phone` | No | Only a number your system has verified. |
| `first_name`, `last_name` | No | Strings. |
| `avatar` | No | Image URL. |
| `custom_attributes` | No | JSON object of attributes to set on the contact. |

```js Node.js theme={null}
const jwt = require('jsonwebtoken');

function mintIdentityToken(user) {
  return jwt.sign(
    { user_id: String(user.id), first_name: user.firstName },
    process.env.CLARITY_IDENTITY_SECRET,
    { algorithm: 'HS256', expiresIn: '10m' }
  );
}
```

```python Python theme={null}
import os
import time

import jwt

def mint_identity_token(user) -> str:
    payload = {
        "user_id": str(user.id),
        "first_name": user.first_name,
        "exp": int(time.time()) + 10 * 60,
    }
    return jwt.encode(payload, os.environ["CLARITY_IDENTITY_SECRET"], algorithm="HS256")
```

Take the `user_id` from your server-side session, never from a parameter the browser sends. Otherwise any visitor could request a token for someone else.

**3. Pass the token to the widget**

Fetch a fresh token on every page load and add it to the snippet's options as `userJwt`. The simplest place is the snippet's `onload`, which runs once the widget script is available:

```js theme={null}
s.onload = async function() {
  o.userJwt = await fetch('/api/clarity-token').then(function (r) { return r.text(); });
  window.anecdoteLiveChatInit(o, window.onAnecdoteSuccess, window.onAnecdoteError);
};
```

`/api/clarity-token` stands for your own authenticated endpoint that returns the token.

> [!WARNING]
>
> Include `email` or `phone` only if your system has verified that the user controls them. A valid token links the chat to any existing contact with that email or phone, including that contact's history from other channels. The payload is signed, not encrypted, so put nothing in it you would not show the visitor.

> [!WARNING]
>
> The identity secret must never appear in your frontend code. Anyone who has it can sign in as any of your visitors.

#### Logging in and out in a single-page app

If the visitor logs in or out after the widget has started, call `anecdoteLiveChatIdentify`:

```js theme={null}
// after login
const userJwt = await fetch('/api/clarity-token').then((r) => r.text());
await window.anecdoteLiveChatIdentify({ userJwt });

// after logout
await window.anecdoteLiveChatIdentify({ userJwt: null });
```

It restarts the widget with the new token and the parameters of your last `anecdoteLiveChatInit` call, so an open panel closes and unsent text is lost. Call it at login or logout, not mid-conversation. `{ userJwt: null }` also drops every identity field and restarts the widget for an anonymous visitor.

The token is checked once, when the session starts. If it expires during a conversation, the conversation carries on.

#### Require a verified identity

In the same **Identity verification** section, **Require a verified identity** refuses any session that states an `externalId` without a valid token. Anonymous visitors can still chat. Turn it on only once your backend issues tokens to every logged-in visitor.

#### Rotate the secret

**Rotate secret** creates a new secret. The previous one stays valid for 7 days, so tokens your backend still signs with it keep working while you deploy the new one. **Revoke previous secret** ends that overlap at once.

## Custom attributes

`customAttributes` sets attributes on the contact when the widget starts. Values can be a string, a number, a boolean, an array of strings (the option ids of a list attribute) or `null` to remove the attribute. Define the attributes your team uses in **Settings > Custom attributes**.

```js theme={null}
"customAttributes": { "plan": "gold", "seat_count": 7, "vip": true }
```

## Customize the widget

**Settings > Widget** controls the widget's look and texts for every channel:

| Section | What you set |
| - | - |
| **Brand** | Primary, background, text and secondary text colours; launcher logo, home logo, assistant avatar; font |
| **Home screen** | Title, subtitle, message card title and subtitle |
| **Conversation** | Assistant name, greeting, greeting out of hours, message box placeholder |
| **Starters and opening** | Up to 10 starter questions a visitor can tap, and whether the chat opens straight into the conversation |
| **Expanded window** | Width and height when the visitor expands the widget |

Your edits are saved as a draft. Click **Publish** to make them live; **History** keeps earlier versions.

The web integration's dialog has per-channel switches: **Enable voice calling** (with the voice agent that answers), **Enable dictation**, **Show the Inbox tab**, **Open straight into the conversation**, **Let visitors verify an email to see their history** and **Host page can open/close the widget**. Click **Save changes** after editing them.

> [!TIP]
>
> Turn off **Show the Inbox tab** if your visitors are not signed in. The widget can list past conversations only for a visitor it can identify.

## Troubleshooting

> **The launcher does not appear**
>
> Check that the snippet is on the page and that the page's domain matches the web integration's **Domain Address** exactly. A different subdomain, such as `example.com` versus `www.example.com`, is a different domain. Check the browser console and network tab for a failed request to `live-chat.leap.onclarity.com`; a Content Security Policy on your site must allow that host for scripts and frames.

> **My own button does not open the chat**
>
> Make sure **Host page can open/close the widget** is on in the integration dialog. If you call `window.anecdoteLiveChatOpen()` from your own script, it exists only after the widget script has loaded: use `window.anecdoteLiveChatOpen?.()`, the `data-anecdote-live-chat` attribute, or `postMessage`, which queue the command until the widget is ready.

> **The session is refused after adding userJwt**
>
> `onAnecdoteError` receives an object whose `code` says why:
>
> | Code | Meaning | Fix |
> | - | - | - |
> | `identity_token_invalid` | The signature did not verify, or a claim such as `iat`, `nbf` or `aud` broke a rule | Sign with the channel's current secret, as shown, with HS256. Check your server clock. |
> | `identity_token_expired` | `exp` has passed | Issue a fresh token and call `anecdoteLiveChatIdentify({ userJwt })`. |
> | `identity_token_malformed` | A claim is missing or has the wrong type, or the token is over 4096 bytes | Check the claims against the table above. `user_id` must be a string. |
> | `identity_token_missing` | **Require a verified identity** is on and the page sent an `externalId` without a token | Send a token with every identified session. |
>
> The widget does not retry a refused token on its own.

> **The Inbox tab is empty**
>
> The widget lists past conversations only for a visitor it can identify. Pass `externalId` or, better, a `userJwt`, or turn on **Let visitors verify an email to see their history**.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.