The widget is a single <script> tag. Drop it on any page,
pass an agent ID, and you're live. The bundle is ≤50KB gzipped, runs in
a Shadow DOM so it can't be styled by your site, and never blocks page
load (it's async).
Paste this just before </body>:
<script
src="https://your-app.test/widget/widget.js?v=ab12cd34"
data-agent-id="01HXY..."
async></script>
The full snippet (with your agent ID and the current cache-bust hash) is on every agent's Settings page next to a copy button.
src — points at /widget/widget.js on your Pitchbar deployment. The ?v=<hash> suffix is the bundle's content hash; it changes whenever the widget is rebuilt, so customers can't get stuck on stale versions cached by a CDN.data-agent-id — the published agent's ULID. The widget's loader reads this attribute and uses it on every /v1/widget/init call.async — non-blocking. The widget appears once the bundle finishes downloading; your page's load metrics are unaffected.data-locale (optional) — force the widget's language (e.g. data-locale="nl"). Omit it and the widget follows your page's <html lang> automatically. See Language below.
The widget renders its UI — launcher label, starter chips, buttons, and
system messages — in the visitor's language, resolved at init
in this order:
data-locale on the script tag, if set — an explicit override (e.g. data-locale="de").<html lang> — so the widget matches whatever language your site is currently rendering. On our own marketing site this is what makes the demo follow the language switcher.Accept-Language, then English.
Regional tags degrade gracefully: en-GB → en,
pt-BR stays pt-BR when that file ships. The page
language wins over the agent default, so a single agent embedded on a
multilingual site speaks each page's language. Translate the launcher
label and starter chips per language in
Translations; anything
without a translation falls back to the original text.
On boot, the loader:
<div> at the bottom of <body> and attaches a Shadow DOM to it.POST /v1/widget/init to get an agent config + JWT + recent history.anon_id in localStorage so the same browser keeps the same conversation across reloads.
Theme is fully agent-driven — see Persona, theme & prompts.
The widget reads theme.position, theme.primary,
theme.accent, theme.radius, and
theme.launcher_label from the init response and renders
accordingly.
There's no per-page customization — the launcher always reads from the agent. If you need a different look on different pages, embed two different agents.
The widget exposes a single global, window.Pitchbar.mount(),
used by the loader to boot. The script tag's async
attribute and auto-mount handle the common case for you, so you don't
typically call this directly.
// Mount the widget into a custom host element (rare).
window.Pitchbar.mount(document.getElementById('chat-host'));
There's no public open / close /
send / on API yet — those are deferred. If
you need to fire conversion analytics on lead capture today, subscribe
to the lead.captured webhook instead (see
Outgoing webhooks).
The widget loads once per page-load, but the conversation persists across in-app navigations as long as the script tag stays in the DOM. You don't need to re-mount it when your router changes routes — the Shadow DOM and JWT survive.
If your SPA fully re-mounts on route changes (e.g. you tear down the body), the widget will re-init and resume the visitor's conversation from the last 24 hours of history.
A POST /v1/widget/init includes:
agent_id — the value from data-agent-id.page_url — the current location.href, used for the "current page" boost in retrieval.anon_id — the visitor's persistent ID from localStorage, generated on first visit.locale — the host page's language (data-locale, else <html lang>), so the widget UI follows the page. Omitted when neither is present.
The server also reads the Origin, Referer, and
Accept-Language headers — Origin for the
allowed-origin check,
Accept-Language for default language detection.
The path component /widget/widget.js is stable across
every deploy. Customers paste the snippet once and never need to
touch it again — bundle updates ship transparently. The
?v=<hash> query mutates on every build, which busts
browser caches on existing pages without changing the URL the customer
pasted on their site.
Internally /widget/widget.js is served by
WidgetBundleController, which streams the latest hashed
bundle named in public/widget/manifest.json with
Cache-Control: no-cache, must-revalidate and a strong
ETag. The hashed file (widget.<hash>.js) is the on-
disk artifact; the stable URL is the public contract.
Pre-fix (2026-05-29) the embed snippet exposed the hashed filename
directly (e.g. widget.abc123.js); every new build rotated
the URL and customers' pasted <script> tags
404'd. The fix landed in AgentController::widgetUrl()
and OnboardingController::buildWidgetSrc().