Skip to main content

What you’ll need

Three values from the dashboard:
ValueFormatWhere to find itWhere it goes
Org IDUUIDSetup > Install (in the snippet)Frontend, in the snippet
Widget keyab_live_...Setup > Install (Widget key card)Frontend, in the snippet — publishable, safe to expose in the browser
Identity secretha_secret_...Settings > SecurityServer 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 under Settings > Security.

The snippet

Paste this before the closing </body> tag on every page where you want the widget:
<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.
The script loads from our CDN and is always up to date. Updates ship automatically — your snippet never needs to change.

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:
<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 for the full pattern syntax (globs work: * matches any chars, ? matches one).
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.

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:
<script>
  var ha = window.HaloAgents.getInstance();
  if (ha) {
    ha.identify("user_123", {
      name: "Jane Doe",
      email: "[email protected]",
      role: "admin",
    });
  }
</script>
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.
For server-rendered pages (PHP, Ruby, Python, etc.), you can inject user data directly:
<script>
  var ha = window.HaloAgents.getInstance();
  if (ha && "<?= $user->id ?>") {
    ha.identify("<?= $user->id ?>", {
      name: "<?= $user->name ?>",
      email: "<?= $user->email ?>",
    });
  }
</script>
See Identify Users for the full reference, including identity verification with JWTs.
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 from your signup server handler. Widget-only identify leaves them out of Contacts and automations until first login. See Authenticated apps with delayed first login.

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.
<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:
<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>/:
<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.
<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:
{
  "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:
curl -s https://api.haloagents.ai/api/sdk/sri \
  | jq -r '.files["0.8.0/haloagents.umd.js"]'
# → sha384-…
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.

Which to use?

NeedRecommended
Get bug fixes and security updates automaticallylatest/ (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):
<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):
<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;
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.

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
Most sites don’t use CSP at all. If you don’t have one configured, you don’t need to add anything.

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. 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.
Wildcards must be scoped to a registrable name. *.example.com is fine, but *.com and *.co.uk are rejected on save.

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.
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)".

Embedding in an iframe

If you load the widget inside an <iframe>, add the allow="microphone" attribute:
<iframe src="..." allow="microphone"></iframe>
The parent page’s Permissions-Policy header must also allow microphone=(self).
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.

Where to go next

Identify Users

Tell Halo who the user is and what company they belong to.

Send Context

Push structured data about the user’s current state.

Customize the Widget

Hide the trigger, open programmatically, theme, and use UI highlighting.

Methods Reference

Every method available on the HaloAgents instance.