1. Install on the main product or marketing site
Start where customers and trial users actually ask questions, not on every property at once.
Copy the snippet from Settings → Widget, paste it before </body>, and you're taking chats. Everything else on this page is optional polish.
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.
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:
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).
The widget is not just a chat box — it's the front door for every lane:
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 HALsetStage() 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 moveStage names must match your pipeline stages in the dashboard (Settings → CRM).
Step-by-step instructions for specific platforms, including theme editing and memberships:
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');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' });onNavigateBy 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)
});/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?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 toolIf 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'
});Start where customers and trial users actually ask questions, not on every property at once.
The widget gets much better once your help-center and operating context are in place.
Use HAL to recommend and draft responses, then choose the level of automation you are comfortable with.
Change colors, positioning, copy, and presentation.
Map signed-in users to contact and company context in HAL.
Secure known-user identity inside the widget.
Server-side identify, track, stage, deal, and property sync.
Improve self-serve support and the quality of AI drafts.