Skip to main content

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:

AttributeMaps toRequired
data-api-urlapiUrlYes
data-org-slugorgSlugYes (unless data-widget-token given)
data-widget-tokenwidgetTokenNo

The widget needs apiUrl and either orgSlug or widgetToken. With only apiUrl it renders but stays inert (no messaging).

Async/defer scripts: document.currentScript is null for async/defer loads, so auto-init falls back to querying for a script[data-auto-init][src*="chat-widget"]. Keep chat-widget in the filename if you rely on auto-init with defer.

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) returns Access-Control-Allow-Origin: * and handles OPTIONS preflight, 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 without credentials: include. If you front the API behind a stricter gateway, make sure the embedding origin is allowed to send Authorization and that WebSocket upgrades to the /notifications namespace are permitted.

5. Content Security Policy​

If the host page sets a CSP, allow:

DirectiveNeedsWhy
script-srcthe CDN origin serving chat-widget.jsload the bundle
connect-srchttps://api.h.work and wss://api.h.workREST + Socket.IO
style-src'unsafe-inline' (or a nonce)the widget injects a <style> block (#humanwork-chat-widget-styles)
img-src / font-srcnot requiredwidget 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.body and a <style> to document.head. All class names are humanwork--prefixed, so it will not collide with host-page CSS, but a strict style-src without '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​