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(), showNewMessage(), setOffset(), 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: setStage() 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).
showNewMessage() opens the widget on a fresh conversation with the message box already filled in. The visitor reviews it and presses send. Leave the text out to open an empty new conversation.
HalWidget.showNewMessage("I'd like to upgrade my plan");
HalWidget.showNewMessage(); // empty new conversationPass tags to tag the visitor's first message, so it arrives in the inbox already sorted — and reaches Slack or Damper if those tags sync there:
HalWidget.showNewMessage("Something looks wrong on the invoices page", {
tags: ["bug", "invoices"],
});Only tags that already exist in the project are applied (create them in the inbox first); unknown tags are ignored, so visitors cannot invent new ones.
Pass view: "articles" to open() to skip the chat and open the widget on your help articles, where visitors can browse and search. Handy for a "Help" link in your app's menu.
HalWidget.open({ view: "articles" }); // help articles page
HalWidget.open(); // the chat, as usualTo have a widget that opens by itself on page load land on the articles, add data-open-view="articles" next to data-open="true" on the script tag (or pass openView: "articles" to init()). To open one specific article, use openArticle().
The distance of the chat button from the screen edges is set in the dashboard (Settings → Widget). If a page has its own bottom bar, cookie banner or floating button in that corner, move the launcher out of the way on just that page with setOffset(). Values are in pixels; leave a side out to keep your dashboard setting.
HalWidget.setOffset({ vertical: 96 }); // 96px from the bottom
HalWidget.setOffset({ horizontal: 32, vertical: 120 });
HalWidget.setOffset(null); // back to the dashboard settingThe change is not saved and lasts until you change it again or the page reloads. In a single-page app, call setOffset(null) when the visitor leaves the page that needed it. You can also pass it at start-up: HalWidget.init({ offset: { vertical: 96 } }).
By default the widget remembers a visitor in the browser's local storage as soon as it loads, so a returning visitor keeps their conversations. If your cookie banner promises that nothing is stored before the visitor chooses, add data-defer-storage="true" to the script tag (or pass deferStorage: true to init()). The widget then keeps everything in memory for the current page and only writes to the device when the visitor opens the chat or sends a message, or when you call HalWidget.enableStorage(), for example after the visitor accepts your cookies.
<script src="https://api.chatwithhal.com/widget.js" data-project-id="YOUR_PROJECT_ID" data-defer-storage="true"></script>
// Later, once your banner records consent:
HalWidget.enableStorage();Until then each page load counts as a new anonymous visitor, so page-visit automations and guide frequency are tracked per page.
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.