B Blengi docs

Embed the widget

Install snippet

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).

The snippet

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.

Two optional attributes

  • data-mount="manual" — load but do nothing until window.Pitchbar.mount() is called; how a consent banner gates the chat.
  • data-context-global="blengiContext" — the name of a window global your pages fill with their own context (page type, category, the product on screen).
  • data-launcher="none" — you have your own chat button; render no floating launcher of ours. Open the panel with window.Pitchbar.open().

Both, the JavaScript API (open, send, close, setContext) and the list of what the widget stores are on the Widget API page.

The inline block

Besides the floating launcher, the same script renders the agent inside a page wherever this block is pasted — a WordPress Custom HTML block, a template, anywhere:

<div data-blengi-inline></div>

Nothing else goes on the site. Copy, layout and colours are managed under Customize → Inline block; the snippet with a copy button is on the agent's Overview page. Details, per-block attributes and the CSS hooks for agencies: Inline embed.

Deploying through Google Tag Manager

The widget can be added as a Custom HTML tag in Google Tag Manager instead of in the site's template — a common ask when a marketing agency owns the container and the site itself is hard to edit. In a tag manager, put the agent id in the script URL, not in a data- attribute:

<script src="https://blengi.com/widget/widget.js?agent=YOUR-AGENT-ID" async></script>

Use an All Pages (Page View) trigger. Nothing else needs configuring. The agent's Overview page carries this exact snippet, with the id filled in, under Google Tag Manager next to the normal one.

Why the URL form: when GTM fires a Custom HTML tag it does not insert your markup as written. It creates a new script element and copies only the src; every data- attribute is dropped, so a tag pasted with data-agent-id loads the file, mounts nothing, and stays silent with no error anywhere. The URL is the one thing that survives, and the loader reads ?agent= from its own src. In your own template either form works; a data-agent-id attribute wins when both are present.

  • Allowed origins still apply — add the domain the container runs on (the agency's test URL now, the production domain later) under Allowed origins, or the widget boots and is refused at /init.
  • Consent Mode. If the tag is gated behind analytics or marketing consent, the widget only appears after the visitor consents. The widget sets one functional identifier (pb_anon_id, to resume the conversation) and no tracking cookies, so most sites fire it as a functional tag; that classification is the site owner's call.
  • Do not load the bundle from an inline script that fetches it, and do not strip the data-agent-id attribute — with no attribute on any script tag the loader has no agent to talk to and stays silent.
  • With GTM on the page, chat events push straight into dataLayer once switched on for the agent.

Two agents on one site, chosen by URL path

When different sections of a site should talk to different agents, keep one Custom HTML tag and let a variable pick the agent:

  1. Create a RegEx Table variable. Input variable: Page Path. One row per section, pattern → agent id, for example ^/waterhardheid/ → the agent's id. Default value: none.
  2. Leave "Full Match" unchecked. With it on, the pattern has to match the whole path, so ^/waterhardheid/ matches /waterhardheid/ and nothing beneath it: every deeper page such as /waterhardheid/gulpen-wittem/ falls back to none, and the widget appears on the section's index page and nowhere else. If you prefer Full Match on, end each pattern with .* instead.
  3. In the tag, put the variable in the URL: <script src="https://blengi.com/widget/widget.js?agent={{Blengi agent}}" async></script>, with an All Pages trigger and an exception (blocking) trigger for Blengi agent equals none. Not a data-agent-id attribute: GTM drops those when it injects the script.
  4. Verify in Preview on a deep page of the section, not on its index page. The index page passes with Full Match on as well, which is exactly how this misconfiguration hides.

Leave any ?v= off the URL in a tag manager: a version pinned in a GTM constant goes stale, and the bundle is served current regardless. ?agent= is the only query the URL needs.

The agent must be allowed on your domain

The widget script is public, so an agent only runs on the domains you list under Settings → Allowed origins. An empty list allows nothing: the script loads, the first request is refused, and the page simply shows no chat with no error on screen.

You rarely have to do this by hand. The first web page you add as a knowledge source sets the origin for you, and the onboarding wizard does the same. If an agent is published with an empty list, its overview page says so and links straight to the field. One entry per site is enough, because https, www and non-www all match.

What's in the URL

  • src — points at /widget/widget.js on your Blengi 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.

Language

The widget renders its UI — launcher label, starter chips, buttons, and system messages — in the visitor's language, resolved at init in this order:

  1. data-locale on the script tag, if set — an explicit override (e.g. data-locale="de").
  2. The host page's <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.
  3. The agent's default language (Settings → the agent's language) when the page declares none.
  4. The browser's Accept-Language, then English.

Regional tags degrade gracefully: en-GBen, 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.

What it injects

On boot, the loader:

  1. Creates a <div> at the bottom of <body> and attaches a Shadow DOM to it.
  2. Renders the launcher (small button) inside the shadow root.
  3. Calls POST /v1/widget/init to get an agent config + JWT + recent history.
  4. Wires up trigger listeners (scroll, idle, exit-intent) per the agent's behavior rules.
  5. Persists a small anon_id in localStorage so the same browser keeps the same conversation across reloads.

Customizing the launcher

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.

Programmatic control

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).

Single-page apps

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.

What gets sent on every init

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.

Versioning & caching

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().