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

# Installation

> Add the Halo widget to your site with a single script tag.

## What you'll need

Three values from the dashboard:

| Value               | Format          | Where to find it                      | Where it goes                                                         |
| ------------------- | --------------- | ------------------------------------- | --------------------------------------------------------------------- |
| **Org ID**          | UUID            | **Setup > Install** (in the snippet)  | Frontend, in the snippet                                              |
| **Widget key**      | `ab_live_...`   | **Setup > Install** (Widget key card) | Frontend, in the snippet — publishable, safe to expose in the browser |
| **Identity secret** | `ha_secret_...` | **Settings > Security**               | **Server only** — used to sign JWTs for identity verification         |

The widget key is like Stripe's `pk_live_` — designed for client-side use. The identity secret is like a Stripe `sk_live_` — never expose it.

## Your widget key

The widget key (`ab_live_...`) is your publishable credential. It goes in the `apiKey` field of `HaloAgents.init()` and in the `Authorization` header if you call the REST API from your backend today.

* **Safe in HTML** — anyone can read it from your page source; that is expected.
* **Copy anytime** — open **Setup > Install** in the dashboard. The full key is shown there with a copy button.
* **Regenerate** — use **Regenerate widget key** on that page if the key is compromised. Your live snippet stops working until you redeploy with the new key.
* **Legacy keys** — workspaces created before April 2026 may have a key we cannot display. Regenerate once to get a copyable key.

To prevent visitors from impersonating another user's `user_id`, enable [Identity Verification](/advanced/identity-verification) under **Settings > Security**.

## The snippet

Paste this before the closing `</body>` tag on every page where you want the widget:

```html theme={null}
<script src="https://cdn.haloagents.ai/sdk/latest/haloagents.umd.js"></script>
<script>
  window.HaloAgents.init({
    orgId: "your-org-id",
    apiKey: "ab_live_xxxxxxxxxxxxxxxx",
  });
</script>
```

The widget appears in the bottom-right corner immediately. No npm packages, no build tools, no framework setup. Works with React, WordPress, Shopify, plain HTML — anything that can include a script tag.

<Tip>
  The script loads from our CDN and is always up to date. Updates ship automatically — your snippet never needs to change.
</Tip>

### Default behavior is quiet

The default config keeps the chat panel closed on page load. Users with unread messages see a small dot on the launcher; the panel only opens when they tap. Same as Intercom. You can drop the snippet on every page (marketing, app, auth flows) and it won't cover a Sign Up or checkout button.

If you want louder behavior on logged-in pages, opt in via the `ui` config:

```html theme={null}
<script>
  window.HaloAgents.init({
    orgId: "your-org-id",
    apiKey: "ab_live_xxxxxxxxxxxxxxxx",
    ui: { autoOpenUnseen: true },
    // When you opt into autoOpenUnseen, you probably also want a
    // carve-out for auth/checkout routes so the panel can't cover them:
    manualOnPaths: ["/signin", "/signup", "/forgot-password", "/oauth/*", "/checkout/*"],
  });
</script>
```

See [Manual mode and per-surface control](/web-widget/customization#manual-mode-and-per-surface-control) for the full pattern syntax (globs work: `*` matches any chars, `?` matches one).

<Note>
  The SDK prints a one-time `console.warn` if `init()` runs on an auth route with `autoOpenUnseen: true` and no `manualOnPaths` covering it. The warning is local to the browser session and has no production effect.
</Note>

## Identifying the user

As soon as you know who the user is (after login), call `identify()` so the AI can personalize and your team can see them in the inbox:

```html theme={null}
<script>
  var ha = window.HaloAgents.getInstance();
  if (ha) {
    ha.identify("user_123", {
      name: "Jane Doe",
      email: "jane@acme.com",
      role: "admin",
    });
  }
</script>
```

<Warning>
  If you skip `identify()` or omit `name` and `email`, tickets and conversations show up as **"Anonymous"** in your inbox. Always pass at least name and email.
</Warning>

For server-rendered pages (PHP, Ruby, Python, etc.), you can inject user data directly:

```html theme={null}
<script>
  var ha = window.HaloAgents.getInstance();
  if (ha && "<?= $user->id ?>") {
    ha.identify("<?= $user->id ?>", {
      name: "<?= $user->name ?>",
      email: "<?= $user->email ?>",
    });
  }
</script>
```

See [Identify Users](/web-widget/identify-users) for the full reference, including identity verification with JWTs.

<Tip>
  If users can **register** before they ever load a page with the widget (email verification, invites, OAuth later), also call [`POST /api/sdk/users/identify`](/api-reference/users) from your signup server handler. Widget-only identify leaves them out of Contacts and outreach campaigns until first login. See [Authenticated apps with delayed first login](/web-widget/identify-users#authenticated-apps-with-delayed-first-login).
</Tip>

## Async loading

To avoid blocking page rendering, use the `async` attribute. **Wait for `window.HaloAgents` before calling `init()`** — `onload` fires when the file finishes downloading, not when the global is registered.

```html theme={null}
<script async
        src="https://cdn.haloagents.ai/sdk/latest/haloagents.umd.js"
        onload="initHaloAgents()"></script>
<script>
  function waitForHaloAgents(onReady, timeoutMs) {
    var deadline = Date.now() + (timeoutMs || 10000);
    (function poll() {
      if (window.HaloAgents) {
        onReady();
        return;
      }
      if (Date.now() >= deadline) {
        console.error("HaloAgents SDK failed to load within timeout");
        return;
      }
      setTimeout(poll, 50);
    })();
  }

  function initHaloAgents() {
    waitForHaloAgents(function () {
      window.HaloAgents.init({
        orgId: "your-org-id",
        apiKey: "ab_live_xxxxxxxxxxxxxxxx",
      });
    });
  }
</script>
```

The same pattern applies when loading the SDK with `next/script`, React effects, or any dynamic script injection.

## WordPress

Add the snippet to your theme's `footer.php` or use a plugin that allows custom scripts:

```php theme={null}
<script src="https://cdn.haloagents.ai/sdk/latest/haloagents.umd.js"></script>
<script>
  var ha = window.HaloAgents.init({
    orgId: "your-org-id",
    apiKey: "ab_live_xxxxxxxxxxxxxxxx",
  });

  <?php if (is_user_logged_in()) : ?>
  ha.identify("<?php echo get_current_user_id(); ?>", {
    name: "<?php echo wp_get_current_user()->display_name; ?>",
    email: "<?php echo wp_get_current_user()->user_email; ?>",
  });
  <?php endif; ?>
</script>
```

## Pinning a specific version

The `latest/` URL is the right default for most sites: every customer automatically gets bug fixes and security updates the moment we publish them. For sites with strict change-management or security-review requirements, you can pin to a specific version instead.

### Versioned URLs

Every published SDK version is also served at `cdn.haloagents.ai/sdk/<version>/`:

```html theme={null}
<script src="https://cdn.haloagents.ai/sdk/0.8.0/haloagents.umd.js"></script>
```

Versioned URLs are immutable. Once published they never change content, so you can review a specific build and trust that the bytes won't drift under you. Trade-off: you have to update the URL by hand to pick up new versions, including security fixes.

### Subresource Integrity (SRI)

For the strongest guarantee, combine version pinning with an `integrity` attribute. The browser will refuse to execute the script if the bytes don't match the hash, which protects you against CDN compromise even if our infrastructure is breached.

```html theme={null}
<script
  src="https://cdn.haloagents.ai/sdk/0.8.0/haloagents.umd.js"
  integrity="sha384-..."
  crossorigin="anonymous"></script>
```

Get the current SHA-384 hash for any released version from `https://api.haloagents.ai/api/sdk/sri`:

```json theme={null}
{
  "version": "0.8.0",
  "files": {
    "0.8.0/haloagents.umd.js": "sha384-...",
    "latest/haloagents.umd.js": "sha384-..."
  }
}
```

Fetch this during your build/deploy pipeline and write the hash directly into your HTML, so you never have to update it by hand:

```bash theme={null}
curl -s https://api.haloagents.ai/api/sdk/sri \
  | jq -r '.files["0.8.0/haloagents.umd.js"]'
# → sha384-…
```

<Note>
  SRI requires the `crossorigin="anonymous"` attribute so the browser is willing to compare bytes across origins. Without it the browser refuses to enforce the integrity check.
</Note>

### Which to use?

| Need                                             | Recommended                |
| ------------------------------------------------ | -------------------------- |
| Get bug fixes and security updates automatically | `latest/` (default)        |
| Strict change-management; vetted builds only     | `<version>/` pinned        |
| Vetted builds + cryptographic tamper protection  | `<version>/` + `integrity` |

## Content Security Policy (CSP)

If your site uses a Content Security Policy, Halo needs permission to connect to the API and load the script.

### Chat only

If you're only using chat (no voice, no Live Help), add:

```
connect-src https://api.haloagents.ai;
script-src https://cdn.haloagents.ai;
```

### Chat + Live Help (streaming avatar)

Live Help uses HeyGen and LiveKit infrastructure, so add all of:

```
connect-src https://api.haloagents.ai https://*.heygen.com wss://*.heygen.com https://*.livekit.cloud wss://*.livekit.cloud;
script-src https://cdn.haloagents.ai;
```

### Examples

In a `<meta>` tag (chat only):

```html theme={null}
<meta http-equiv="Content-Security-Policy" content="
  default-src 'self';
  script-src 'self' https://cdn.haloagents.ai;
  connect-src 'self' https://api.haloagents.ai;
">
```

In a `<meta>` tag (chat + Live Help):

```html theme={null}
<meta http-equiv="Content-Security-Policy" content="
  default-src 'self';
  script-src 'self' https://cdn.haloagents.ai;
  connect-src 'self' https://api.haloagents.ai https://*.heygen.com wss://*.heygen.com https://*.livekit.cloud wss://*.livekit.cloud;
">
```

Or via HTTP header:

```
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.haloagents.ai; connect-src 'self' https://api.haloagents.ai https://*.heygen.com wss://*.heygen.com https://*.livekit.cloud wss://*.livekit.cloud;
```

<Warning>
  If you see **"Failed to fetch"** or **"Refused to connect because it violates the document's Content Security Policy"** in your browser console, your CSP is blocking Halo. Add the directives above.
</Warning>

### Diagnosing CSP issues

1. Open DevTools (F12 or Cmd+Opt+I)
2. Go to **Console**
3. Look for red errors mentioning "Content Security Policy" or "connect-src"
4. Add the missing domains

<Note>
  Most sites don't use CSP at all. If you don't have one configured, you don't need to add anything.
</Note>

## Origin allow-list (optional)

By default the widget loads on any domain. If you want to lock it down to a specific list of sites, turn on **Allowed Domains** under Setup → Install in the dashboard.

```
example.com
app.example.com
*.example.com
```

When enabled, every SDK request whose `Origin` header doesn't match an entry is rejected with a 403. Per-user impersonation is a separate concern handled by [Identity Verification](/web-widget/identify-users#identity-verification-jwt). Allowed Domains is purely an embedding control.

### Local development

The browser sets `Origin: http://localhost:3000` for code running on your dev server, which won't match a production domain entry. Add the loopback hostname you actually use to your allow-list:

```
example.com
*.example.com
127.0.0.1
localhost
```

Either is accepted. Develop against `http://127.0.0.1:3000` if you added `127.0.0.1`, or `http://localhost:3000` if you added `localhost`. Same model as Intercom's Trusted Domains.

<Note>
  Wildcards must be scoped to a registrable name. `*.example.com` is fine, but `*.com` and `*.co.uk` are rejected on save.
</Note>

## Microphone permissions (voice features)

If your agent uses **voice input**, Halo needs access to the user's microphone via `getUserMedia`. Most sites work out of the box, but two configurations can block access.

### Permissions-Policy header

If your server sends `Permissions-Policy` (or legacy `Feature-Policy`) restricting `microphone`, voice fails with **"Permissions policy violation: microphone is not allowed in this document"**.

Fix:

```
Permissions-Policy: microphone=(self)
```

If your header has `microphone=()` (fully blocked), update it.

<Tip>
  In Next.js, set this in `next.config.js` via the `headers()` function. In Nginx: `add_header Permissions-Policy "microphone=(self)"`. In Apache: `Header set Permissions-Policy "microphone=(self)"`.
</Tip>

### Embedding in an iframe

If you load the widget inside an `<iframe>`, add the `allow="microphone"` attribute:

```html theme={null}
<iframe src="..." allow="microphone"></iframe>
```

The parent page's `Permissions-Policy` header must also allow `microphone=(self)`.

<Warning>
  If voice fails with **"Permissions policy violation: microphone is not allowed in this document"** or **"NotAllowedError: Permission denied"**, check both the header and any iframe `allow` attributes.
</Warning>

## Where to go next

<CardGroup cols={2}>
  <Card title="Identify Users" icon="user" href="/web-widget/identify-users">
    Tell Halo who the user is and what company they belong to.
  </Card>

  <Card title="Send Context" icon="database" href="/web-widget/send-context">
    Push structured data about the user's current state.
  </Card>

  <Card title="Customize the Widget" icon="paintbrush" href="/web-widget/customization">
    Hide the trigger, open programmatically, theme, and use UI highlighting.
  </Card>

  <Card title="Methods Reference" icon="code" href="/web-widget/methods">
    Every method available on the HaloAgents instance.
  </Card>
</CardGroup>
