B Blengi docs

Embed the widget

Allowed origins

The widget script is public on purpose — anyone can fetch /widget/widget.js. That makes allowed origins the trust boundary that stops a third party from embedding your snippet on their own site and burning your quota.

The contract

Every POST /v1/widget/init reads the request's Origin header (or Referer as a fallback) and checks it against the agent's allowed_origins list. The rules are:

  1. Empty list → 403. Deny everywhere. New agents start empty until you add at least one origin.
  2. Wildcard "*" → allow. Opt-in escape hatch for internal tools and demos. Never set as a default.
  3. Otherwise → normalised scheme://host match, made forgiving so you enter the domain once:
    • A bare domain with no scheme (stappsokken.nl) defaults to https.
    • www and non-www are the same — listing example.com, www.example.com, or https://example.com all match both https://example.com and https://www.example.com.
    • Every other subdomain stays distinct. https://example.com still does not permit https://app.example.comwww is the only subdomain treated as the same site.

Every privileged call re-checks Origin

The init endpoint is where the JWT gets minted — but the JWT is a bearer token, and bearer tokens can be exfiltrated (leaked log, browser dev-tools, XSS on a victim site, MITM on cleartext). To stop a stolen JWT from being replayed from attacker.example, every privileged widget endpoint runs through the VerifyWidgetOrigin middleware, which re-validates the request Origin against the JWT-bound agent's allowed_origins on every call:

  • POST /v1/widget/messages
  • POST /v1/widget/messages/stream (SSE)
  • POST /v1/widget/leads
  • POST /v1/widget/request-human
  • POST /v1/widget/events
  • POST /v1/widget/typing
  • POST /v1/widget/satisfaction
  • POST /v1/widget/coupon/apply
  • POST /v1/widget/conversation/clear
  • GET /v1/widget/conversation/messages
  • DELETE /v1/widget/me

Policy is identical to /v1/widget/init — empty list = deny, "*" = allow (including no-Origin), specific entries = exact normalised match — so a request that init would have approved can never be 403'd by the post-init middleware, and a request that init would have denied can never sneak past either.

Strict subdomain matching

This is the rule that catches people off guard, so it deserves its own callout:

Listing https://thecodestudio.com in allowed_origins does not permit https://pitchbar.thecodestudio.com. Subdomains are independent — list each one explicitly. This prevents an attacker who controls a subdomain (via DNS or shared hosting) from inheriting trust from the parent. The one exception is www: www.example.com and example.com are treated as the same site, so you never list both.

If you actually want additional subdomains, list them individually (www is already covered):

https://example.com
https://app.example.com
https://docs.example.com

Adding origins

From the agent's Settings page (/app/agents/{id}/settings), the Allowed origins card has a textarea — one origin per line. Save updates the agent immediately; new init requests use the new list within seconds.

You can enter just the domain — the scheme defaults to https and www is matched automatically. How a few inputs are read:

You enterMatches
example.comhttps://example.com and https://www.example.com
www.example.comsame as above (www and non-www)
http://localhost:3000http://localhost:3000 — keep the scheme + port for local http development
*.example.comnothing — wildcards aren't supported except as a lone "*"

Testing locally

During development, add http://localhost:3000 (or whatever port you're using) to the agent's allowed origins. Don't use "*" for this — leaving it on by accident in production leaves the agent open.

What happens on rejection

A request from a disallowed origin gets a JSON 403:

{
    "error": {
        "code": "origin_forbidden",
        "message": "Origin is not allowed for this agent."
    }
}

The widget's loader handles this gracefully — the launcher disappears silently rather than throwing a console error, so visitors never see a broken UI. The 403 is logged on the platform side so you can watch for abuse patterns.

What about same-host origins

The check uses scheme + host, so http vs. https is distinct (as it should be). And different ports are different origins (http://localhost:3000http://localhost:3001).

Wildcards: when to use, when not

"*" exists for cases where you genuinely don't know the origin in advance:

  • Internal demo agents that get embedded on every prospect's preview site.
  • Sandbox / preview environments where origin churns daily.

For production agents, never. The cost of forgetting "*" is that anyone who finds your data-agent-id can drain your quota. The cost of an explicit list is one minute per new origin.

Where should this agent appear? — the path-level companion

Allowed origins draws the trust boundary at the domain level (only https://shop.example.com can load the widget). Where should this agent appear? is its sibling, one level down: which paths within an allowed origin the agent shows on. One list of patterns, read one of three ways:

ModeThe agent appears onTypical use
Everywhere every page of the allowed domains; the list is ignored the default
Everywhere except these paths every page but the ones a pattern matches keep the bot off your own /admin, /checkout or /account flows
Only on these paths the pages a pattern matches, and nowhere else two specialised agents on one site — one only on /waterhardheid/*, the other everywhere except it

Each entry is a glob — * is the only wildcard, and matches across slashes greedily. Comparison is case-insensitive against window.location.pathname:

PatternMatchesDoesn't match
/admin /admin, /Admin /admin/users (use /admin/* for that)
/waterhardheid/* /waterhardheid, /waterhardheid/, /waterhardheid/gulpen-wittem/ /feiten-en-fabels/waterhardheid
/checkout /checkout /checkout/confirm
/account/* /account/profile, /account/security /Help/account

How it works at runtime

The agent's path_mode and restricted_paths ride the same POST /v1/widget/init response as the rest of the config, and the cached copy the widget paints from on a return visit carries both, so a page that is out never flashes the bar for half a second. When the page is out, the bar never mounts, the trigger engine never starts, and no further HTTP rides on that page. The init call itself does happen (the server is the source of truth).

Only on these paths with an empty list shows the agent everywhere rather than nowhere: picking the mode before typing a pattern must not make an agent vanish from the whole site.

Two agents on one website

Give the section agent Only on these paths with /waterhardheid/*, and the general agent Everywhere except these paths with the same pattern. Both scripts can then be installed on every page — through the theme or a tag manager with an All Pages trigger — and each agent decides for itself whether this page is its own. On a page where the first script's agent is out, it hands the page to the other, so a visitor sees one widget and never two, and never none. One page, one /init per tag, as before. No path lookup in the tag manager is needed for that; see Install snippet for the tag-manager recipe if you prefer routing there.

Authoring

From the agent's Settings page, the Where should this agent appear? card has the three options and, for the second and third, a textarea with one path per line. Up to 32 entries, each up to 200 characters. Agents that had restricted paths before this setting existed were moved to Everywhere except these paths automatically, so nothing changed for them.

Why this is a separate knob from auth

The platform also auto-suppresses the marketing demo widget on authenticated admin/customer routes via a server-side check in the Inertia root layout — that's a hard guarantee that doesn't depend on agent config. restricted_paths is the buyer-side extension: even on a fully unauthenticated marketing site, /checkout shouldn't be cluttered with a sales chat bot.

How auto-index uses origins

Auto-index uses the same allow-list, but with a twist when "*" is set: the page URL being auto-indexed must match the visitor's actual Origin header. That stops a malicious page from auto-indexing arbitrary third-party domains via the wildcard.