Setup · Widget

The widget is one script tag.

Copy the snippet from Settings → Widget, paste it before </body>, and you're taking chats. Everything else on this page is optional polish.

Basic install

Grab your snippet from the dashboard — it carries your project ID:

<script
  src="https://api.chatwithhal.com/widget.js"
  data-project-id="YOUR_PROJECT_ID"
  async
></script>

The widget renders inside a Shadow DOM, so your site's CSS can't break it and its styles can't leak into your site. No build step, no framework requirements — it works on Shopify, WordPress, Webflow, React, or hand-written HTML.

Language

The widget detects the visitor's browser language and matches it against its 14 built-in locales (EN, DE, FR, ES, IT, PT, NL, PL, RU, JA, ZH, KO, SL, HR). Two project settings override the default behavior:

  • Default language — used when the browser locale isn't available.
  • Force default — ignore detection entirely and always show the default.

You can also pin a language per embed: <script ... data-locale="de">, or force the widget open on load with data-open="true" and a delay with data-open-delay="3000" (milliseconds).

What the widget feeds

The widget is not just a chat box — it's the front door for every lane:

  • Inbox — live conversations with typing indicators and attachments.
  • CRM — identification and stage sync (see visitor identification).
  • AI — context for reply drafts and pre-handoff answers.
  • Analytics — visit-to-chat conversion and the visitor heatmap.

JavaScript API

The widget exposes a single global, HalWidget. All methods are safe to call after load — calls queue until the widget is ready. In addition to the methods below there are open(), close(), init(), trackPageView(), and survey/NPS triggers.

HalWidget.identify({
  user_id: "usr_123",          // your stable id
  email: "mia@novaleaf.com",
  name: "Mia Kovač",
  metadata: { plan: "trial" }  // custom properties
});

HalWidget.track("checkout_completed", 1200);
HalWidget.setStage("customer");          // CRM stage sync
HalWidget.setDeal({ deal_value: 1800 }); // deal value → pipeline
HalWidget.set({ seats: 12, mrr: 149 });  // merge custom properties
HalWidget.resetIdentity();               // log the visitor out of HAL

CRM stage sync

setStage() moves the visitor's company through your pipeline. By default it uses advance-only mode: product signals can move a deal forward but never backward — a downgraded user won't drag a won deal back to Trial. Pass an explicit mode when you need exact control:

HalWidget.setStage("evaluating");               // advance-only is the default
HalWidget.setStage("churned",    "exact");           // allow a backward move

Stage names must match your pipeline stages in the dashboard (Settings → CRM).

Platform-specific guides

Step-by-step instructions for specific platforms, including theme editing and memberships:

Next steps

Interactive guides

Guides are step-by-step walkthroughs your team authors in HAL (dashboard or MCP). They can fire automatically on a page, be offered to a visitor who asks in chat, or be started from your own code by slug — the stable identifier shown on the guide, which survives renames.

// Start a guide programmatically, e.g. after a milestone
await window.HalWidget.showGuide('create-and-send-your-first-invoice');

Point the guide at its own page

A guide can declare a start location — a path template against visitor properties, such as {{app_base_path}}/documents/add/invoice. When a visitor starts it from anywhere else in your app, the widget takes them there first instead of showing tooltips that anchor to nothing.

Set the scope property wherever your app knows the current context, and update it when the visitor switches workspace or environment:

window.HalWidget.set({ app_base_path: '/app/ws_12345' });

Wire your router with onNavigate

By default the widget navigates with history.pushState, which does not drive every client-side router. Pass onNavigate so your own router performs the move — this also keeps any unsaved-work prompts in your hands:

window.HalWidget.init({
  projectId: 'your-project-id',
  onNavigate: (path) => router.navigate(path)
});

Registration timing

/widget.js is a tiny loader that fetches the real bundle, so code running on the loader's load event cannot see window.HalWidget yet. Either wait for it:

document.addEventListener('hal:ready', (event) => {
  event.detail.widget.init({
    projectId: 'your-project-id',
    onNavigate: (path) => router.navigate(path)
  });
});

Or call init() with your options any time after window.HalWidget appears — options registered before the widget finishes mounting are buffered and applied at mount. Calling init() again with just the changed options (for example init({ locale })) updates the running widget.

<!-- The trap: /widget.js is a loader stub. Its load event fires before
     the real bundle executes, so the obvious patterns do nothing — no
     error is thrown, the call is simply lost:

script.onload = () => HalWidget.init({ onNavigate })   // silently dropped
window.HalWidget?.init({ onNavigate })                 // silently dropped

     Register a hal:ready listener anywhere before the bundle loads, or
     poll for window.HalWidget, and register from there. -->
  • showGuide() accepts the guide's slug or its original name; the slug is the stable one
  • If a scope property is missing, a guide with a start location starts in popup mode instead of navigating — it never navigates to a half-built path
  • If the visitor is already on the start page, no navigation happens
  • Test any guide without spending its frequency limit: add ?hal_guide_preview=<guide-id> to a page URL, and ?hal_guide_debug=1 to validate its selectors against the page you are logged into — results appear in the browser console and via the MCP validate_guide tool

Reset identity on logout

If your app supports login/logout, reset the widget when the user signs out. This clears the current browser-side widget identity and remounts the widget with a fresh anonymous visitor so the next user can be identified cleanly.

// After your app logs the user out
await window.HalWidget.resetIdentity();

// Later, after a different user logs in
await window.HalWidget.identify({
  user_id: 'user-456',
  email: 'new-user@example.com',
  user_hash: 'server-generated-hmac'
});
  • Use this on shared browsers or any SaaS app where different accounts can use the same device
  • Call it before identifying the next user
  • It resets the client-side widget identity; it does not delete CRM records or historical conversations in HAL

Recommended rollout

1. Install on the main product or marketing site

Start where customers and trial users actually ask questions, not on every property at once.

2. Add knowledge before expecting strong replies

The widget gets much better once your help-center and operating context are in place.

3. Keep human approval where needed

Use HAL to recommend and draft responses, then choose the level of automation you are comfortable with.

Related docs