Integration Guide: Embedding the Chat Widget
How to embed the h.work customer-facing chat widget on a client website. For the architecture, auth model, and threading semantics see the feature spec at ../features/embeddable-chat-widget.md.
The widget is a single standalone bundle (@humanwork/chat-widget,
packages/chat-widget/) with zero runtime
dependencies. It injects a floating chat button into the host page and talks
to the h.work API directly from the browser using a short-lived visitor JWT.
1. Get the bundleβ
The build (bun run build in packages/chat-widget/) emits an IIFE bundle at
dist/chat-widget.js exposing the global HumanworkChatWidget. Host it on a
CDN/static origin you control and reference it from the client page.
packages/chat-widget/
src/index.js β entry; reads data-* attrs, optional auto-init
src/widget.js β ChatWidget class (UI + session + socket)
dist/chat-widget.js β built bundle (global: HumanworkChatWidget)
2. Embed optionsβ
Option A β Auto-init via <script> (no JS required)β
Add data-auto-init plus the config attributes; the widget mounts on
DOMContentLoaded and is reachable afterwards as window.humanworkChat.
<script
src="https://cdn.example.com/chat-widget.js"
data-auto-init
data-api-url="https://api.h.work"
data-org-slug="acme"
></script>
data-* attributes read by index.js:
| Attribute | Maps to | Required |
|---|---|---|
data-api-url | apiUrl | Yes |
data-org-slug | orgSlug | Yes (unless data-widget-token given) |
data-widget-token | widgetToken | No |
The widget needs
apiUrland eitherorgSlugorwidgetToken. With onlyapiUrlit renders but stays inert (no messaging).
Async/defer scripts:
document.currentScriptisnullforasync/deferloads, so auto-init falls back to querying for ascript[data-auto-init][src*="chat-widget"]. Keepchat-widgetin the filename if you rely on auto-init withdefer.
Option B β Programmatic (full control)β
Omit data-auto-init and construct the widget yourself. This is the path for
custom theming, deferred mounting, or lifecycle control.
<script src="https://cdn.example.com/chat-widget.js"></script>
<script>
const widget = new HumanworkChatWidget.ChatWidget({
apiUrl: "https://api.h.work",
orgSlug: "acme",
theme: { primary: "#0F766E" },
// widgetToken: "<pre-issued JWT>", // optional, skips the session call
// visitorId: "<uuid>", // optional, pin a visitor identity
});
widget.init();
// Lifecycle API:
// widget.open(); widget.close(); widget.toggle(); widget.destroy();
</script>
3. How auth works at embed timeβ
You do not ship any secret to the browser. On first interaction the widget
calls POST {apiUrl}/public/widget/session with { orgSlug, visitorId } and
receives a 24h visitor JWT (see the auth model).
The visitor UUID is persisted in localStorage under
humanwork.chat.visitorId.{orgSlug} so returning visitors keep their threads
on the same browser. Token refresh and 401 re-mint are automatic.
If you prefer to mint the token server-side (e.g. to tie a widget session to a
known end user), pass it as widgetToken/data-widget-token; supply orgSlug
too so the widget can silently refresh when the token nears expiry.
4. CORSβ
- Session endpoint (
/public/widget/session) returnsAccess-Control-Allow-Origin: *and handlesOPTIONSpreflight, so it can be called from any embedding origin. - Conversation + socket calls go through the standard API CORS layer
(
api/src/common/cors/hwork-cors-allowlist.ts). The widget uses bearer tokens, not cookies, specifically so cross-origin embedding works withoutcredentials: include. If you front the API behind a stricter gateway, make sure the embedding origin is allowed to sendAuthorizationand that WebSocket upgrades to the/notificationsnamespace are permitted.
5. Content Security Policyβ
If the host page sets a CSP, allow:
| Directive | Needs | Why |
|---|---|---|
script-src | the CDN origin serving chat-widget.js | load the bundle |
connect-src | https://api.h.work and wss://api.h.work | REST + Socket.IO |
style-src | 'unsafe-inline' (or a nonce) | the widget injects a <style> block (#humanwork-chat-widget-styles) |
img-src / font-src | not required | widget uses an emoji glyph + system fonts, no external assets |
Example (adjust origins per environment β staging uses api.h852.work):
Content-Security-Policy:
script-src 'self' https://cdn.example.com;
connect-src 'self' https://api.h.work wss://api.h.work;
style-src 'self' 'unsafe-inline';
The widget appends a fixed-position container to
document.bodyand a<style>todocument.head. All class names arehumanwork--prefixed, so it will not collide with host-page CSS, but a strictstyle-srcwithout'unsafe-inline'/nonce will break the injected styles.
6. Themingβ
Pass theme.primary (any CSS color). It drives the toggle button, header, and
user message bubbles via the --humanwork-primary custom property. The panel is
360Γ500 on desktop and goes near-fullscreen below 480px width.
7. Removing the widgetβ
Call widget.destroy() (programmatic) to disconnect the socket, clear timers,
and remove both the container and injected styles from the DOM. Auto-init
exposes the instance as window.humanworkChat for this.
See alsoβ
- Feature spec β ../features/embeddable-chat-widget.md
- Channels overview (web is one ingress channel) β ../features/channels.md
- Public + conversation API surface β ../api/reference.md
- Package README β
packages/chat-widget/README.md