bubuu/api/docs
API Documentation·Frontend SDK
03 · The embeddable chat

Lead
Widget

A 2 KB facade that paints on first frame. The full widget streams in behind it. Embedded into any page, framework, or CMS in two lines of HTML.

Web ComponentShadow-isolatedDrop-in scriptEN · DE

At a glance

Bubuu's lead widget is a Custom Element — <bubuu-chat> — that drops a guided real-estate conversation into any page. It mounts inside a Shadow DOM, inheriting none of the host site's CSS and leaking none of its own. No frameworks to align, no global styles to fight.

The widget ships in two halves. A 2 KB loader renders the chat bubble on first paint; the full bundle preloads in the background and takes over on click. Visitors on cold connections still see the bubble before the page finishes settling.

Loader bundle
~2 KB over the wire
Widget bundle
~210 KB over the wire
Element
<bubuu-chat>
Distribution
Hosted JS · drop-in
Isolation
Shadow DOM
Languages
English · German
Themes
Light · Dark · Auto

Install

Two scripts, one element. Drop the loader on every page where the widget should be reachable; the chat bubble renders immediately and the full widget streams in behind it.

01

Facade loader

Recommended

Renders the chat bubble in ~2 KB. The full bubuu-chat.js bundle preloads in the background and is promoted on click or hover.

<script src="https://chat.bubuu.ai/bubuu-loader.js" async></script>
<bubuu-chat partner-id="your-partner-id"></bubuu-chat>
02

Direct embed

Full bundle

Use this when the page is a conversion target and you want the widget interactive on first paint. Ships the full ~210 KB bundle up-front — skip if you care about Core Web Vitals on cold visits.

<script src="https://chat.bubuu.ai/bubuu-chat.js" async></script>
<bubuu-chat partner-id="your-partner-id"></bubuu-chat>

Architecture

Two scripts, in sequence. The loader is tiny enough to be inlined on every page; the widget is large enough that you would not want it on every page. The facade pattern lets both be true.

  1. 01

    Loader registers the element

    bubuu-loader.js defines <bubuu-chat> as a 2 KB facade and renders a chat bubble inside a Shadow DOM. The full bundle starts preloading immediately.

  2. 02

    Bubble paints on first frame

    No framework boot, no hydration cost. The bubble is a single button with inline styles, so it shows up even on cold cache and slow connections.

  3. 03

    Visitor opens the chat

    On hover, the preloaded widget bundle is already cached. On click, the cached bundle is promoted from background to foreground.

  4. 04

    Widget takes over in place

    The full widget upgrades the same DOM node, inheriting every attribute you set on it. The conversation opens immediately — visitors never see a second click.

Attributes

Every option is a plain HTML attribute on <bubuu-chat>. Set them once in the markup or change them at runtime with element.setAttribute(…). Attributes flagged reactive re-render the widget without losing chat state.

partner-idstringrequired

Identifies your account on the Bubuu backend. Every chat and lead created by this instance is attributed to this partner.

chat-idstring

Resumes an existing conversation by its server-issued ID. When omitted, a new chat is created on the first user message. Useful for deep-linking back into a previous session from email or a CRM.

lang'en' | 'de'default · enreactive

UI language. Changing this at runtime swaps the locale without clearing messages — the conversation continues in the new language.

theme'light' | 'dark' | 'auto'default · lightreactive

Color scheme. auto follows the host page's prefers-color-scheme and reacts to system theme changes. The widget runs inside a Shadow DOM, so this never affects host styles.

regionstringreactive

ISO country or region code used to pick locale-specific copy, phone formats, and address parsers. Defaults to the partner's configured region when absent.

avatar-urlstring (URL)

Override the agent avatar shown next to assistant messages. Falls back to the partner's brand avatar when omitted.

phonestringreactive

Display-form phone number shown in the chat header (e.g. +1 (830) 532-5995). Must contain only dialable characters — the widget derives its tel: link by stripping everything else. Falls back to the partner registry, then no phone line at all, when omitted.

launcher-textstring

Call-to-action label shown on the launcher. Its presence turns the round chat bubble into a wider pill (icon + label). Read once when the widget mounts.

launcher-iconstring (emoji | URL)

Icon inside the launcher. Pass an emoji or glyph (e.g. 💬) to render it as text, or an image URL (http(s)://, a data:image/… URI, or a root-relative /… path) to render an <img>. Falls back to the default chat icon when omitted.

position'bottom-right' | 'bottom-left'default · bottom-right

Which bottom corner the widget anchors to. Case-insensitive; an unrecognised value falls back to bottom-right. For finer control — exact offsets, your own class, media queries — see the Styling API.

page-eventsboolean attributedefault · offreactive

Opt in to the widget's session events (conversation started, lead submitted, welcome screen, language/region). Presence enables them; page-events="false" explicitly disables. Off by default, so the widget never dispatches into a page that did not ask for it. The two launcher events are always emitted and need no opt-in.

contextJSON stringreactive

Page, visitor, and metadata context for the AI as a JSON object — { page, visitor, metadata }. Sent with every message so answers stay grounded in what the visitor is viewing. Malformed JSON is ignored with a console warning; the widget still loads. For objects from JS or a framework, prefer the context property or setContext() — see Context.

Context

Tell the AI what the visitor is looking at. Attach page, visitor, free-form metadata, and the page content itself, and the widget sends it on every message, so the assistant grounds its answers in the exact listing on screen and can pre-fill lead capture. Context rides the existing chat channel — no extra requests, and it survives the facade→full-widget upgrade.

page{ url, title, referrer }

What the visitor is viewing. Auto-derived from the host page (location.href, document.title, document.referrer) when you omit it; anything you pass wins per field.

visitor{ id, name, email }

Known visitor identity. Used to pre-fill the lead form. user is accepted as a friendly alias for visitor.

metadataRecord<string, string>

Free-form key/value bag. Put listing identifiers here — listingId, an OpenImmo or RESO ref, price, location — and the backend resolves them into the matched property. Values are coerced to strings; the backend sanitizes and size-limits them.

pageContentstring | { body, type }

The page content the AI should read, handed straight to the backend so it can skip scraping the page. Pass a string (treated as the body) or { body, type }, where type is a format hint (e.g. markdown vs HTML). Omit it to let the backend fetch the page itself.

Three ways to set it

Use the context attribute for static, server-rendered markup; the context property to bind an object (it replaces the current context); and setContext(patch) to merge a partial update at runtime. getContext() returns the current value. All three reach the same place. Malformed JSON in the attribute is ignored with a console warning — the widget still loads.

<bubuu-chat
  partner-id="acme-real-estate"
  context='{"metadata":{"listingId":"AT-1234"}}'
></bubuu-chat>

In your framework

Frameworks bind objects to the element's context property. React 19, Vue, Angular, Svelte, and Preact all do this for you. React 18 and below stringify object props, so reach for a ref and call setContext() instead.

// React 19 binds an object prop to the element's "context" property.
<bubuu-chat partner-id="acme-real-estate" context={ctx} />

Timing is automatic: context set before the chat opens lands on the first message, and an update pushed mid-session applies to the next one. setContext merges, so when the page changes (e.g. SPA navigation) send the new pageContent in full — a partial patch keeps the previous page's body. Updating context never clears the thread or drops the connection — it behaves like the reactive lang and theme attributes. The widget only reads window and document; it adds nothing to the host's global scope.

Theming

The widget paints inside a Shadow DOM — host styles can't bleed in, widget styles can't bleed out. Click a mode to retheme the live widget; the switch lands in place, no reload.

<bubuu-chat partner-id="your-partner-id" theme="auto"></bubuu-chat>

Styling API

Restyle the widget with CSS custom properties on <bubuu-chat> — set them in a stylesheet or inline; they inherit through the widget's shadow DOM. A token you don't set keeps the Bubuu default. A token you do set always wins, even over a partner theme.

--bubuu-primarycolor

Accent color used across the widget: launcher, buttons, links, focus rings.

--bubuu-on-primarycolor

Text/icon color drawn on top of the accent.

--bubuu-surfacecolor

Panel background.

--bubuu-textcolor

Body text color.

--bubuu-text-mutedcolor

Secondary text color.

--bubuu-bordercolor

Border and divider color.

--bubuu-radiuslength

Corner rounding for messages and controls.

--bubuu-fontfont-family

Typeface for the whole widget.

--bubuu-launcher-bgcolordefault · --bubuu-primary

Launcher bubble background.

--bubuu-launcher-colorcolordefault · --bubuu-on-primary

Launcher icon/text color.

bubuu-chat {
  --bubuu-primary: #0b5fff; /* accent: launcher, buttons, links, focus */
  --bubuu-on-primary: #ffffff; /* text/icon on the accent */
  --bubuu-surface: #ffffff; /* panel background */
  --bubuu-text: #1a2230; /* body text */
  --bubuu-text-muted: #6b7280; /* secondary text */
  --bubuu-border: #e4e6ed; /* borders / dividers */
  --bubuu-radius: 12px; /* message & control rounding */
  --bubuu-font: 'Inter', sans-serif; /* typeface */
  --bubuu-launcher-bg: #0b5fff; /* launcher bubble (defaults to --bubuu-primary) */
  --bubuu-launcher-color: #ffffff; /* launcher icon/text (defaults to --bubuu-on-primary) */
}

Placement

The widget sits 24px from the bottom-right corner by default. Use the position attribute to flip corners, or these tokens to nudge it. The launcher and the chat panel always move together. <bubuu-chat> is itself the fixed-position anchor in your page, so you can also just style it — your own class names and media queries work, and no !important is needed.

--bubuu-offset-xlengthdefault · 24px

Distance from the side edge the widget is anchored to.

--bubuu-offset-ylengthdefault · 24px

Distance from the bottom edge.

--bubuu-panel-inset-bottomlengthdefault · 0px

Height of anything you pin to the bottom of the page (sticky ad slot, mobile nav). Lifts the full-screen mobile chat sheet clear of it so your bar stays visible. No effect on larger screens, where the panel already follows --bubuu-offset-y.

--bubuu-z-indexintegerdefault · 1000

Stacking order of the whole widget. Raise it if your own overlays cover the launcher.

/* Nudge it away from your own floating UI */
bubuu-chat {
  --bubuu-offset-x: 40px;  /* from the anchored side edge (default 24px) */
  --bubuu-offset-y: 120px; /* from the bottom edge        (default 24px) */
  --bubuu-z-index: 10000;  /* above your own overlays     (default 1000) */

  /* Mobile only: clears a sticky bottom bar of your own. */
  --bubuu-panel-inset-bottom: 60px;
}

/* Or style the host element directly — your own class, your own media
   queries. No !important needed: your page's rules always win. */
.chat-above-cookie-bar {
  bottom: 140px;
}

@media (max-width: 768px) {
  bubuu-chat {
    bottom: 88px; /* clear a sticky mobile nav */
  }
}

On phones the chat panel opens full-screen, whatever the position — only the launcher follows the corner. If your page pins something to the bottom on mobile (an ad slot, a nav bar), it will paint over the panel's footer and its main button. Either raise --bubuu-z-index so the chat covers your bar while open, or set --bubuu-panel-inset-bottom to your bar's height so the sheet stops on top of it and your bar stays visible.

Mount <bubuu-chat> as a direct child of <body>: transform, filter, will-change or contain on an ancestor re-bases fixed positioning onto that ancestor and will strand the widget mid-page.

Builder

Configure the widget visually, preview the launcher, welcome, conversation and lead form screens together, and copy the embed code.

Open the builder →

Language

Pick a locale to flip the live widget. UI strings re-render in place — the chat thread, the open WebSocket, and scroll position survive the switch.

const widget = document.querySelector('bubuu-chat-full, bubuu-chat');

widget?.setAttribute('lang', 'de');

Events

The widget reports what the visitor does as native CustomEvents dispatched on the <bubuu-chat> element. They bubble, so listening on document is enough. Every payload carries a timestamp (epoch ms) and nothing else beyond what is listed below — no names, emails, or message text — so you can pipe them straight into your own analytics.

Launcher

The two signals from the closed launcher, before any conversation exists. No opt-in needed, and each fires at most once per page load.

bubuu:hover{ partnerId, timestamp }always on

The pointer entered the closed launcher for the first time. An early intent signal — useful for measuring how many visitors notice the bubble versus actually open it.

bubuu:open{ partnerId, timestamp }always on

The visitor clicked the launcher and the chat is opening. Closing and reopening the chat later does not repeat it.

Session

Everything that happens inside the chat is opt-in: add the page-events attribute and these start flowing. Without it the widget never dispatches anything into a page that did not ask for it.

bubuu:conversation-started{ timestamp }page-events

The visitor sent their first message. One event per conversation, not per session — resuming an earlier conversation with chat-id does not re-fire it.

bubuu:welcome-shown{ timestamp }page-events

The AI-disclosure welcome screen was shown. It appears on every open until the visitor accepts it, so treat this like a pageview rather than a one-shot.

bubuu:welcome-acknowledged{ timestamp }page-events

The visitor accepted the welcome screen. Fires once per visitor — returning visitors skip the screen, so this does not repeat on later visits.

bubuu:lead-submitted{ callPreference, timestamp }page-events

The lead form was submitted successfully — the conversion moment. callPreference is how the visitor wants to be contacted: IMMEDIATE, SCHEDULE_LATER, or SCHEDULED. The contact details they typed are not included.

bubuu:panel-opened{ timestamp }page-events

The chat panel opened. Unlike bubuu:open, which fires once on the click that loads the full widget, this is one half of a pair that repeats on every toggle — use it with bubuu:panel-closed to track whether the chat is on screen right now.

bubuu:panel-closed{ timestamp }page-events

The chat panel closed. The conversation is kept, so a later open resumes it rather than starting over.

bubuu:language-changed{ language, partnerId, timestamp }page-events

The widget's UI language changed — either the assistant switched it mid-conversation (it follows the language the visitor writes in) or you set the lang attribute. Only real changes emit: re-applying the active locale is a no-op, so mirroring this event back onto the element cannot loop. Clearing the attribute resets to English silently, without an event.

bubuu:region-changed{ region, partnerId, timestamp }page-events

The active region changed, as an upper-cased ISO country code. Region has no visible UI in the chat, so this is your only signal that it moved. Same no-op and reset rules as bubuu:language-changed.

<!-- Presence of "page-events" enables the session events.
     page-events="false" explicitly disables them. -->
<bubuu-chat partner-id="acme-real-estate" page-events></bubuu-chat>

A single document listener catches the session events, even when you nest <bubuu-chat> inside your own shadow root. The two launcher events do not cross a shadow boundary — if the widget lives inside one, listen on the element itself. Listeners are the entire surface: the widget adds nothing to your page's global scope.

Cookbook

A few patterns that cover most production embeds. Each one stands on its own — copy, swap in your IDs, ship.

01

Use in any framework

<bubuu-chat> is a standard Custom Element. Load the loader once, then render the tag anywhere — React, Vue, Svelte, Angular, or plain HTML all treat it as a native element.

01 — Load the loader
<script src="https://chat.bubuu.ai/bubuu-loader.js" async></script>
02 — Render the tag
<bubuu-chat partner-id="your-partner-id" theme="auto"></bubuu-chat>
02

Resume a chat from a link

Drop the visitor straight back into their last conversation. Pass chat-id when linking from email follow-ups or saved-search alerts and the widget restores the thread.

<bubuu-chat
  partner-id="acme-real-estate"
  chat-id="chat_01HXYZ..."
></bubuu-chat>
03

Ground the AI in the property

On a listing page, push the property the visitor is viewing. The assistant answers about that listing and the lead is saved with it. Re-run setContext whenever the listing changes — the thread and connection stay put.

import { useEffect, useRef } from 'react';

export function ListingChat({ listing }: { listing: Listing }) {
  const ref = useRef<(HTMLElement & { setContext(c: object): void }) | null>(null);

  // Re-ground the AI whenever the visitor opens a different listing.
  useEffect(() => {
    ref.current?.setContext({
      page: { url: location.href, title: document.title },
      metadata: {
        listingId: listing.id,
        price: String(listing.price),
        city: listing.city,
      },
    });
  }, [listing]);

  return <bubuu-chat ref={ref} partner-id="acme-real-estate" />;
}