Blog
Tutorials

Embedding a Knowledge-Base Chat Widget with Lead Capture

A practical guide to embedding Simmetric Chat on your site: sandboxed iframe loader, server-resolved model pin, lead capture, and CORS setup.

Simmetric Team8 min read15/09/2026

Turn your documentation into a front-door assistant

Your knowledge base already contains the answers your visitors are looking for. An embedded chat widget puts those answers on your website as a conversational front door: visitors ask questions, the widget runs the same hybrid RAG pipeline and agent infrastructure your internal teams use, and answers arrive with citations from the workspaces you choose to expose. This tutorial walks through embedding the widget, controlling what it answers with, capturing leads, and getting CORS right - from an admin panel to a working snippet in minutes.

The two-step embed: a loader and a sandboxed iframe

The widget is built around a security-first embedding pattern. Instead of injecting markup and styles into your page, the widget ships in two steps:

  1. The loader script. You add one script tag to your site, pointing at your widget service with your widget ID. The loader is a small JavaScript file that creates an iframe on the host page.
  2. The iframe panel. The iframe loads a self-contained Preact application - built as an IIFE bundle with CSS inlined - that renders the floating action button, the chat panel, and every interaction inside its own document.
html
<script src="https://your-simmetric-chat.com/widget/loader.js?widgetId=WIDGET_ID"></script>

Why an iframe? Because string interpolation of untrusted content into a host page is the classic XSS path. Running the widget UI inside a sandboxed, opaque-origin iframe removes that risk class entirely: the chat content never executes in your page's context. The old string-interpolated embed routes were removed from the product on that exact rationale, so the two-step pattern is not one option among several - it is the embed.

Communication between the iframe and your page happens over a small, versioned postMessage protocol (simmetric:* message types) that handles the FAB open/close dance, FAB color repaint, and session storage handshakes. Both sides validate every inbound message against the exact contentWindow it expects, so forged cross-iframe messages are ignored.

Creating the widget and pointing it at knowledge

Everything starts in the admin panel:

  1. Create a widget from Settings → Widgets. Widget creation is license-gated (widget_enabled) and respects the plan's numeric widget limit.
  2. Link the widget to one or more workspaces. The workspace whitelist replaces all previous links atomically, so layering knowledge-base access is a matter of which workspaces you attach.
  3. Configure branding: primary color, bot name, logos, position (bottom-right or bottom-left). Community-tier deployments get sensible defaults applied when fields are empty; full customization and white-labeling are enterprise flags.
  4. Copy the embed snippet into your site's HTML.

Because widgets are workspace-scoped, the knowledge a widget can draw on is exactly the union of its linked workspaces' knowledge bases - nothing more. A support widget can see your public docs workspace without ever touching internal project data.

The response model pin: resolved server-side

One recent addition matters for quality control: each widget's configuration can select a specific LLM provider and model that serves that widget's chat responses. The pin is stored on the widget row and resolved server-side on every chat request - it is never supplied by the client, the widget proxy, or the visitor's browser. A pinned model takes priority over the workspace default, and unset widgets keep the standard resolution chain. Practically: your marketing widget can run on the exact model you vetted for public answers, regardless of what anyone tampers with client-side.

Lead capture that respects visitors

The widget's lead capture is designed for the reality of a public form: abuse concentrates where data is collected, so the lead endpoint is deliberately throttled to 3 requests per hour per source (with the limit surfaced to the user). The flow:

  • The visitor sees a PII consent gate before chatting - an explicit acknowledgment of how data is handled, not a buried policy link.
  • A lead card collects contact details during or after the conversation, and submissions land in the widget's lead inbox in the admin panel, with CSV export for your pipeline.
  • Sessions are anonymous by design: the server creates a session with a 256-bit token and a 24-hour expiry, and every subsequent request is validated against it. Per-session limits (20 messages per hour, 5 conversations per day) return a 429 with a retryAfter hint when exceeded, and the UI shows a rate-limit notice instead of a silent failure.

Rate limits are part of the trust story, not an afterthought: a public chat surface should degrade politely under load or abuse, and the lead limiter is the tightest gate of the three widget-side limiters.

CORS: fail-closed by default

Widget traffic crosses origins by definition, so CORS is part of the security surface rather than an afterthought. Each widget carries its own allowedOrigins list, editable from the admin panel. At runtime, a dynamic CORS middleware validates every request's Origin header against that widget's list - requests from disallowed origins are rejected on preflight, and the middleware fails closed: if the origin check cannot consult the database, access is denied rather than granted.

Configure it in one pass:

  • Add your site's origin(s) (scheme and host, no trailing slash games) to the widget's allowed origins.
  • Remember that subdomain variants count as distinct origins - example.com and www.example.com are two entries.
  • Use https:// origins in production; the widget service itself speaks the same URL rules as the rest of the stack.

Everything else - the loader URL, the iframe, the session bootstrap, the SSE chat stream - uses the same host you deploy the widget service on, so there is exactly one place where origin trust is decided: the widget's allowlist.

What runs where

A quick orientation map, because the widget touches three processes:

  • Main server - widget CRUD and branding, workspace linking, the internal API the widget service calls with its API key, and lead/analytics endpoints for admins. The whole internal router returns 402 when the widget feature is off.
  • Widget service - the public embed surface: loader JS, iframe HTML, session creation, cached config delivery, the SSE chat proxy toward the main server, and the lead endpoint. It authenticates to the main server with a WIDGET_API_KEY and carries its own rate limiters.
  • The iframe - the Preact IIFE client with the chat lifecycle: session creation, SSE streaming token by token, citations display, suggested questions, and consent gates.

The chat proxy forwards the same event format as the internal streaming API - status messages, plan announcements, streaming tokens, citations, and terminal done/error events - so anything your team sees in a workspace chat, your visitors experience in the widget.

Launch checklist

  • Widget created, license feature enabled, workspaces linked
  • Branding set; white-label flag if the widget must not carry product chrome
  • Response model pin set on the widget row (server-resolved)
  • Allowed origins covering every site and subdomain that will embed it
  • Lead capture verified end-to-end: consent gate, submission, admin inbox, CSV export
  • Rate limits understood: 3 leads/hour, 20 messages/hour per session, 5 conversations/day

From there, the widget is a drop-in: one script tag, one allowlist, one knowledge base you already trust. When your visitors ask questions, they get answers grounded in exactly the workspaces you chose - with a lead capture pipeline that is polite to humans and strict with bots.

Read the full guide in the docs →

Related articles