> ## 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.

# Authentication

> Authorize web calls with an allowed-domain list or with short-lived tokens signed on your server.

Every web call must prove it is allowed to reach your agent. The SDK supports two methods: an **allowed-domain list** for public sites, and **server-generated tokens** for apps where you control who can start a call. Bot protection, if you turn it on, applies on top of either method.

| | Allowed domains | Server-generated token |
| - | - | - |
| How it works | Callers checks the page's `Origin` against your list | Your server signs a token; the SDK sends it with each request |
| Code on your server | None | A few lines (HMAC-SHA256) |
| Works from any domain | No, listed hostnames only | Yes, a valid token is accepted from any origin |
| Local development | `localhost` with a port won't match | Works on any port |
| Best for | Public marketing pages | Logged-in apps, staging, local development |

## Allowed domains

Add the hostnames where the widget runs on the agent's **Channels** > **Widget** > **Embed on Web** tab, under **Your Domains**. Then use the agent ID alone:

```html theme={"dark"}
<script src="https://cdn.callers.ai/sdk.js" data-agent="YOUR_AGENT_ID"></script>
```

Matching rules (verified against the API):

* The dashboard stores hostnames only, such as `www.example.com`. A full URL you paste is reduced to its hostname.
* Matching is exact. `example.com` does not cover `www.example.com` or `app.example.com`; add each one.
* Wildcards (`*.example.com`) are rejected.
* Because ports are dropped, a page on `http://localhost:3000` does not match `localhost`. Use a token for local development.
* An empty list blocks every domain-based call with `No allowed origins configured. Add allowed domains or use token-based auth.`

## Server-generated tokens

A token is a short string your server signs with a secret. The browser only ever sees the token, never the secret.

### Which secret to use

Either secret works:

* The agent's **web call secret**, shown on the **Channels** > **Widget** > **Custom** tab under **Authenticate**. It is created when you turn on the widget.
* Any of your organization's [API keys](https://docs.callers.ai/documentation/using-callers/generate-api-key.md).

> [!WARNING]
>
> Never put the web call secret or an API key in browser code, in a script tag, or in a mobile app bundle. Anyone who reads it can start calls on your account. Only your server should sign tokens.

### Token format

```
{agentId}.{expiry}.{signature}
```

| Part | Value |
| - | - |
| `agentId` | The agent ID |
| `expiry` | Unix time in **seconds** after which the token is rejected |
| `signature` | Lowercase hex HMAC-SHA256 of the string `{agentId}.{expiry}`, keyed with your secret |

The SDK reads the agent ID from the first part of the token, so you don't need to pass `campaignId` separately.

### Generate a token

```javascript Node.js theme={"dark"}
import crypto from 'node:crypto';

export function createCallersToken(agentId, ttlSeconds = 3600) {
  const expiry = Math.floor(Date.now() / 1000) + ttlSeconds;
  const payload = `${agentId}.${expiry}`;
  const signature = crypto
    .createHmac('sha256', process.env.CALLERS_WEB_CALL_SECRET)
    .update(payload)
    .digest('hex');

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

```python Python theme={"dark"}
import hashlib
import hmac
import os
import time


def create_callers_token(agent_id: str, ttl_seconds: int = 3600) -> str:
    expiry = int(time.time()) + ttl_seconds
    payload = f"{agent_id}.{expiry}"
    signature = hmac.new(
        os.environ["CALLERS_WEB_CALL_SECRET"].encode(),
        payload.encode(),
        hashlib.sha256,
    ).hexdigest()
    return f"{payload}.{signature}"
```

```javascript SDK helper theme={"dark"}
// generateToken uses Web Crypto, so it runs in Node.js 18+ and other
// runtimes with crypto.subtle. Tokens it creates are valid for 1 hour.
const { generateToken } = window.CallersWebSdk; // or import from the package
const token = await generateToken('YOUR_AGENT_ID', process.env.CALLERS_WEB_CALL_SECRET);
```

Expose it from an endpoint your frontend can call, ideally one that requires the visitor to be signed in:

```javascript Express theme={"dark"}
app.get('/callers-token', requireLogin, (req, res) => {
  res.json({ token: createCallersToken(process.env.CALLERS_AGENT_ID) });
});
```

### Use the token

```javascript Programmatic theme={"dark"}
const { token } = await fetch('/callers-token').then((r) => r.json());
const webCall = new CallersWebSdk.WebCall({ token });
```

```html Script tag theme={"dark"}
<!-- Render the token into the page on the server -->
<script src="https://cdn.callers.ai/sdk.js" data-token="{{ callers_token }}"></script>
```

### Token lifetime

You choose the lifetime when you set `expiry`; the SDK helper uses one hour. The token is checked when the call starts **and** on later requests during the call: typed messages, form answers, page summaries and the transcript stream. So:

* Make the token live at least as long as your agent's maximum call duration, plus the time a visitor may leave the page open before calling. One hour suits most sites.
* Mint a fresh token on each page load instead of caching one.
* An expired or wrongly signed token fails with `Invalid web call token` (HTTP 401).

> [!NOTE]
>
> The SDK sends the token as a `token` query parameter on the transcript stream, because browsers can't add headers to `EventSource` requests. Keep lifetimes short for this reason.

## The `secret` and `apiKey` options

`WebCall` also accepts `secret` or `apiKey` and signs tokens in the browser. Use these only in local test pages (the [playground](https://docs.callers.ai/sdk/playground.md) does this). Never ship them in production code.

## Related

> **[Bot protection](https://docs.callers.ai/sdk/bot-protection.md)**
>
> Add a Turnstile check on top of either method.

> **[Troubleshooting](https://docs.callers.ai/sdk/troubleshooting.md)**
>
> Fix origin and token errors.


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