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

# Installing the Widget

> Add the Callers widget to your website so visitors can talk to your AI agent - by voice or text - directly in the browser, without dialing a phone number.

## What Is the Widget?

The widget is a small piece of Callers that lives on your website. Visitors see a floating button in the corner of the page; when they click it, they can start a conversation with your AI agent right there in the browser - **by voice or by text**, with no phone number and no app to install.

Because the conversation runs on your own website, you already know who the visitor is. You can pass their name, email, or plan into the campaign so the agent greets them by name and skips the questions you already have answers to.

<CardGroup cols={2}>
  <Card title="No Phone Number Needed" icon="globe">
    Conversations run over the browser, so there are no per-minute telephony costs and no number to provision.
  </Card>

  <Card title="Voice or Text" icon="comments">
    Visitors choose how they want to talk. The same campaign script handles both.
  </Card>

  <Card title="Context From Your Site" icon="user-check">
    Pass what you already know about the visitor into the campaign as variables.
  </Card>

  <Card title="Two Installation Paths" icon="code-branch">
    A one-line script tag for most sites, or the Web SDK when you need full control.
  </Card>
</CardGroup>

<Info>
  Widget campaigns are enabled per account. If you don't see the **Widget** option described below, contact [support@callers.ai](mailto:support@callers.ai) to have it turned on.
</Info>

***

## Step 1: Create a Widget Campaign

<Steps>
  <Step title="Start a new campaign">
    From your dashboard, click **New Campaign**.
  </Step>

  <Step title="Open General Settings">
    In the campaign editor, open the **General Settings** modal.
  </Step>

  <Step title="Select Widget as the channel">
    In the channel row - **Voice**, **Text**, **Email**, **Widget** - choose **Widget**.
  </Step>

  <Step title="Name your campaign">
    Give the campaign a descriptive name so you can find it later, then close the modal.
  </Step>
</Steps>

<Frame caption="Selecting Widget as the campaign channel in General Settings">
  <img src="https://mintcdn.com/voxiaai/MknMf2SQMIhWFOwC/images/widget-campaign-type.png?fit=max&auto=format&n=MknMf2SQMIhWFOwC&q=85&s=0a11de6bb5e6e91c850111c8e14d2dac" alt="General Settings modal with the Widget channel selected" style={{borderRadius: '10px'}} width="1496" height="1753" data-path="images/widget-campaign-type.png" />
</Frame>

### What Changes When You Pick Widget

Selecting **Widget** reconfigures the campaign automatically:

* The campaign becomes **Incoming**. Your visitors start the conversation - the widget never dials out.
* **Outgoing phone numbers are cleared**, and incoming phone calls are switched off. A widget campaign is reached through your website only.
* A **web call secret** is generated for the campaign. You'll need it only if you build a custom integration ([see below](#custom-integration-web-sdk)).

<Note>
  Switching an existing campaign to **Widget** removes its phone numbers. If you want to keep a phone campaign running, create a separate campaign for the widget instead of converting the one you already have.
</Note>

### Build Your Script

A widget campaign uses the same script builder as a voice campaign. Write your steps, add actions, and set up variables exactly as you normally would - see [Campaign Script](/documentation/build-your-campaign/campaign-script) and [Campaign Variables](/documentation/build-your-campaign/campaign-variables).

Any variables you define here become available to the widget, so you can pass real values from your website into the conversation.

<Warning>
  Save your campaign before continuing. The installation code is generated from the saved campaign, so the installer stays empty until the campaign exists.
</Warning>

***

## Step 2: Open the Installer

In the campaign toolbar, click the **plug** icon to open the **Install the widget** window.

<Frame caption="The plug icon in the campaign toolbar opens the installer">
  <img src="https://mintcdn.com/voxiaai/MknMf2SQMIhWFOwC/images/widget-install-button.png?fit=max&auto=format&n=MknMf2SQMIhWFOwC&q=85&s=871f1ee88cec41db98e691371b96aa3d" alt="Campaign toolbar with the widget connection button highlighted" style={{borderRadius: '10px'}} width="852" height="189" data-path="images/widget-install-button.png" />
</Frame>

<Note>
  This icon appears only on widget campaigns - it takes the place of the **Channels** button used by phone and messaging campaigns.
</Note>

The window has two tabs:

| Tab              | Use it when                                                                                |
| ---------------- | ------------------------------------------------------------------------------------------ |
| **Embed on Web** | You want the standard widget on your site. One script tag, no development work.            |
| **Custom**       | You need your own UI, your own launch button, or authenticated visitors. Uses the Web SDK. |

Most sites should use **Embed on Web**.

***

## Embed on Web

<Frame caption="The Embed on Web tab: domains, bot protection, and the installation snippet">
  <img src="https://mintcdn.com/voxiaai/MknMf2SQMIhWFOwC/images/widget-install-embed.png?fit=max&auto=format&n=MknMf2SQMIhWFOwC&q=85&s=f9fae740f5ecd7c141ef6f053febab3b" alt="Install the widget modal showing the Embed on Web tab" style={{borderRadius: '10px'}} width="1458" height="1768" data-path="images/widget-install-embed.png" />
</Frame>

### 1. Add Your Domains

List every domain the widget is allowed to run on. Type a domain and press **Enter** to add it; each one appears as a removable chip.

This list is a security boundary, not a suggestion. When a visitor opens the widget, Callers checks the page's origin against this list and refuses the conversation if it isn't there.

<Warning>
  **Domains are matched exactly.** A few consequences worth knowing before you start:

  * **Wildcards are rejected.** `*.example.com` will not be accepted.
  * **Subdomains are separate entries.** Adding `example.com` does *not* cover `app.example.com` or `www.example.com` - add each one you actually use.
  * **Ports are not stored.** A local address like `localhost:3000` is saved as `localhost`, which will not match a request coming from port 3000. To test locally, use the **Custom** integration with a token instead, since token authentication skips the domain check.
</Warning>

If the list is empty, the widget cannot start a conversation at all - you'll see the error `No allowed origins configured`.

### 2. Bot Protection

Bot protection stops automated traffic from burning through your conversation minutes. Callers uses **Cloudflare Turnstile**, which is free.

<Steps>
  <Step title="Create a Turnstile widget">
    Open the [Cloudflare Turnstile dashboard](https://dash.cloudflare.com/?to=/:account/turnstile) and create a new widget.
  </Step>

  <Step title="Add your website domains">
    Enter the same domains you added in the previous step. Cloudflare will only issue challenges for domains listed here.
  </Step>

  <Step title="Copy both keys into Callers">
    Cloudflare gives you a **Site key** and a **Secret key**. Paste both into the matching fields in the installer.
  </Step>
</Steps>

<Info>
  **Both keys are required.** Bot protection switches on only when the site key *and* the secret key are filled in - fill in just one and it stays off. The secret key lives in Callers because verification happens on our servers, not in the visitor's browser.
</Info>

Once enabled, bot protection applies to **both** installation methods - the embed script and the Web SDK. Unlike the domain list, a token does not bypass it.

<Note>
  After a visitor passes a challenge and the conversation ends, they must close and reopen the widget to start another one. This is intentional: each conversation gets its own challenge.
</Note>

### 3. Copy the Code

Copy the snippet and paste it into your HTML, just before the closing `</body>` tag, on every page where you want the widget to appear.

```html theme={null}
<script
  src="https://cdn.callers.ai/sdk.js"
  data-campaign="YOUR_CAMPAIGN_ID"
></script>
```

That's the whole installation. The floating button appears in the corner of the page as soon as the script loads.

#### Passing Visitor Information

If your campaign defines variables, the snippet includes a `data-vars` attribute pre-filled with placeholders:

```html theme={null}
<script
  src="https://cdn.callers.ai/sdk.js"
  data-campaign="YOUR_CAMPAIGN_ID"
  data-vars='{"name":"{{ name }}","email":"{{ email }}"}'
></script>
```

<Warning>
  The `{{ name }}` placeholders are **not** filled in automatically. Replace them with real values from your page or template before going live - otherwise your agent will greet visitors with the literal text `{{ name }}`.
</Warning>

Rendered by your server or templating engine, the final tag should look like this:

```html theme={null}
<script
  src="https://cdn.callers.ai/sdk.js"
  data-campaign="YOUR_CAMPAIGN_ID"
  data-vars='{"name":"Jane Doe","email":"jane@example.com"}'
></script>
```

#### Script Tag Reference

| Attribute       | Description                                                                                                                                                  |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `data-campaign` | **Required** (unless you use `data-token`). The campaign the widget connects to.                                                                             |
| `data-token`    | A pre-generated authentication token. Use instead of `data-campaign` for token-based authentication - see [Custom Integration](#custom-integration-web-sdk). |
| `data-vars`     | JSON object of campaign variables, e.g. `'{"name":"Jane"}'`. Must be valid JSON.                                                                             |
| `data-theme`    | `"light"`, `"dark"`, or a full theme configuration as JSON.                                                                                                  |
| `data-fab-text` | Label shown next to the phone icon on the floating button. Leave it out for an icon-only round button.                                                       |
| `data-show-fab` | Set to `"false"` to hide the floating button and launch the widget from your own code.                                                                       |

***

## Custom Integration (Web SDK)

Use the **Custom** tab when the standard widget isn't enough - you want your own launch button, you need the conversation embedded inside your page layout, or your visitors are logged in and you want to authenticate them properly.

<Frame caption="The Custom tab: install the SDK, authenticate, and initialize">
  <img src="https://mintcdn.com/voxiaai/MknMf2SQMIhWFOwC/images/widget-install-custom.png?fit=max&auto=format&n=MknMf2SQMIhWFOwC&q=85&s=2231718dcc398c26514ecf76b56e2914" alt="Install the widget modal showing the Custom SDK tab" style={{borderRadius: '10px'}} width="1460" height="1785" data-path="images/widget-install-custom.png" />
</Frame>

### 1. Install the SDK

```bash theme={null}
npm install @callers/web-sdk
```

### 2. Authenticate

Instead of checking the visitor's domain, the SDK authenticates with a **short-lived token** that you generate on your own backend.

The **Authenticate** step in the installer shows your campaign's **web call secret**. Copy it and store it as a server-side environment variable.

<Warning>
  **Never put the secret in browser code.** Anyone who reads your page source could mint their own tokens and run conversations on your account. The secret belongs on your backend only - your frontend should only ever receive the finished token.
</Warning>

#### Generating a Token

The SDK ships the helper, so on a JavaScript backend it's one call:

```javascript theme={null}
import { generateToken } from '@callers/web-sdk';

const token = await generateToken(campaignId, process.env.CALLERS_WEB_CALL_SECRET);
```

That returns a token valid for one hour. Send it to your frontend and pass it to `WebCall`.

#### Generating a Token Without the SDK

If you'd rather not add the SDK as a server dependency, the format is simple enough to build yourself. A token is three dot-separated parts: the campaign ID, an expiry timestamp in seconds, and an HMAC-SHA256 signature of the first two parts, hex-encoded.

```
{campaignId}.{expiry}.{signature}
```

Here it is in Node. It ports directly to any language with an HMAC library - the only thing that has to match is the payload string and hex encoding:

```javascript theme={null}
import crypto from 'node:crypto';

const TOKEN_TTL_SECONDS = 3600; // 1 hour

function generateWebCallToken(campaignId, secret, ttlSeconds = TOKEN_TTL_SECONDS) {
  const expiry = Math.floor(Date.now() / 1000) + ttlSeconds;
  const payload = `${campaignId}.${expiry}`;
  const signature = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');

  return `${payload}.${signature}`;
}
```

Expose it through an endpoint your frontend can call - ideally one that requires the visitor to be logged in, so only real users can obtain a token.

<Info>
  **Tokens expire after one hour.** Generate a fresh token each time a visitor loads the page rather than caching one. Requests with an expired token are rejected with `Invalid web call token`.
</Info>

<Note>
  Token authentication **replaces** the domain allowlist - a valid token is accepted from any origin. Bot protection, if enabled, still applies.
</Note>

### 3. Initialize the SDK

Pass the token your backend generated, along with any campaign variables:

```javascript theme={null}
import { WebCall } from '@callers/web-sdk';

const webCall = new WebCall({
  token,
  vars: {
    name: 'Jane Doe',
    email: 'jane@example.com',
  },
});

webCall.call();
```

`webCall.call()` starts a voice conversation immediately. To show the floating button and let the visitor start it themselves, create the instance without calling `call()` - the widget renders and waits.

### Try It in the Playground

Before wiring the SDK into your own application, you can exercise it against a real campaign in the **[SDK Playground](https://sdk-playground.callers.dev/)**. Enter your campaign ID and secret, click **Load SDK**, and a working widget appears on the page - with every SDK method available as a button, a live event feed, and a running transcript.

<Frame caption="A campaign loaded in the playground - the widget is live in the bottom-right corner">
  <img src="https://mintcdn.com/voxiaai/I8ipeRKqJlPIbgp7/images/widget-playground-connected-campaign.png?fit=max&auto=format&n=I8ipeRKqJlPIbgp7&q=85&s=1a7d7cc75aadbfcb99f8794e5791261b" alt="SDK Playground with a campaign connected, the SDK loaded, and the widget button running on the page" style={{borderRadius: '10px'}} width="3785" height="1793" data-path="images/widget-playground-connected-campaign.png" />
</Frame>

It doubles as a visual theme builder. See [Styling the Widget](/documentation/widget-campaigns/styling-the-widget) for that side of it.

***

## Verify the Installation

<Steps>
  <Step title="Load the page">
    Open a page where you pasted the snippet. The floating button should appear in the corner within a second or two.
  </Step>

  <Step title="Start a conversation">
    Click the button and allow microphone access when the browser asks. Try both the voice option and the text composer.
  </Step>

  <Step title="Check the transcript">
    Open the campaign in Callers and confirm the conversation appears with its full transcript.
  </Step>
</Steps>

### Common Errors

| Message                                                    | What it means                                                                 | How to fix it                                                                                                                  |
| ---------------------------------------------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `No allowed origins configured`                            | The domain list is empty.                                                     | Add at least one domain in **Your Domains**, or switch to token authentication.                                                |
| `Origin '...' is not allowed for this campaign`            | The page's domain isn't on the list.                                          | Add that exact domain - including the subdomain - and save the campaign.                                                       |
| `Origin header is required`                                | The page was opened without an origin, typically from a local `file://` path. | Serve the page over `http://` or `https://` instead of opening the file directly.                                              |
| `Web calls not enabled for campaign`                       | The campaign isn't set up as a widget campaign.                               | Open **General Settings** and select **Widget** as the channel.                                                                |
| `Bot verification is required` / `Bot verification failed` | Turnstile is enabled but the challenge wasn't completed or didn't validate.   | Confirm the site key and secret key are both correct and that the page's domain is listed in your Cloudflare Turnstile widget. |
| `Invalid web call token`                                   | The token is expired, malformed, or signed with the wrong secret.             | Generate a fresh token on your backend using the campaign's web call secret.                                                   |

<Accordion title="The widget doesn't appear at all">
  Check that the script tag is inside `<body>` and not blocked by a content blocker or a Content Security Policy. If your site sets a CSP, allow scripts from `https://cdn.callers.ai`.

  Then check the tag itself: if neither `data-campaign` nor `data-token` is present, the SDK stops without rendering anything and without logging - a silent page is almost always a missing or misspelled `data-campaign`. Malformed `data-vars` or `data-theme` JSON does get reported in the browser console.
</Accordion>

<Accordion title="The agent greets visitors with {{ name }}">
  Your `data-vars` placeholders were never replaced. The installer generates them as templates; your page or server needs to substitute real values before the HTML reaches the browser.
</Accordion>

<Accordion title="It works in production but not on localhost">
  The domain field stores hostnames without ports, so `localhost:3000` is saved as `localhost` and won't match. Use the **Custom** integration with a token for local development - token authentication skips the domain check entirely.
</Accordion>

***

## Next Steps

* Add [campaign actions](/documentation/build-your-campaign/campaign-actions) so your agent can book meetings or push data to your CRM during a conversation.
* Review [Analytics & Insights](/documentation/using-callers/analytics) to see how widget conversations are performing.
* Set up [webhooks](/documentation/webhook/advanced-webhook) to receive conversation results in your own systems.
