Guides

The widget & email channel

The customer-service half of Bubblio: a chat teammate in the corner of your site, answering from your own knowledge and calling tools on your own backend — and the same teammate reading your support email. One brain, one playbook, one conversation pool.

Next.js: two files and one edit

terminalbash
npm install @bubblio/server @bubblio/widget

Grab an API key from the dashboard (it starts with bbl_), put it in .env.local as BUBBLIO_API_KEY, then:

app/api/bubblio/session/route.tsts
// app/api/bubblio/session/route.ts — mints sessions on YOUR server.
// Your Bubblio API key never reaches the browser.
import { createBubblioSession } from '@bubblio/server'

export async function POST() {
  const session = await createBubblioSession({
    bubblioApiKey: process.env.BUBBLIO_API_KEY!,
    personality: 'You are Aria, a friendly support agent for Acme. Keep answers short.',
  })
  return Response.json(session)
}
app/layout.tsxtsx
// app/layout.tsx — two added lines put the bubble on every page.
import { BubblioWidget } from '@bubblio/widget'
import './globals.css'

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        {children}
        <BubblioWidget config={{ serverUrl: '/api/bubblio/session' }} />
      </body>
    </html>
  )
}

Run npm run dev and the bubble sits in the corner of every page. Only want it on one page? Put the same two lines in that page's page.tsx instead.

Any site: one script tag

No React, no build step — plain HTML, Webflow, Wix, anywhere you can paste custom code. The widget bundles everything (its own React included), styles itself inline, and floats above the page; it's one ~300 KB gzipped file loaded with defer, so it never blocks rendering.

index.html — data attributeshtml
<!-- Before </body>. That's the whole integration. -->
<script defer
  src="https://cdn.jsdelivr.net/npm/@bubblio/widget@0.11/dist/embed.global.js"
  data-server-url="https://yoursite.com/api/bubblio/session"
  data-name="Aria"></script>
index.html — window.bubblioConfightml
<!-- Configure from JS instead: set window.bubblioConfig ABOVE the embed
     tag (inline scripts run before defer'd ones, so the order is always safe). -->
<script>
  window.bubblioConfig = { serverUrl: 'https://yoursite.com/api/bubblio/session' }
</script>
<script defer src="https://cdn.jsdelivr.net/npm/@bubblio/widget@0.11/dist/embed.global.js"></script>

The embed talks to a session endpoint on your server (same as the Next.js route above). When your website and that endpoint live on different domains — always the case on Webflow/Wix — include the CORS header:

the session endpoint, CORS includedts
import { createBubblioSession } from '@bubblio/server'

// POST /api/bubblio/session — for embeds hosted on another domain
// (always the case on Webflow/Wix), allow your site in via CORS:
export async function POST() {
  const session = await createBubblioSession({
    bubblioApiKey: process.env.BUBBLIO_API_KEY!,
    personality: 'You are Aria, support agent for Acme.',
  })
  return Response.json(session, {
    headers: { 'Access-Control-Allow-Origin': 'https://yoursite.com' },
  })
}
Builder rule of thumbPage-level code injection works (Webflow Footer code, Squarespace Code Injection, Shopify theme.liquid, Framer end-of-body); iframe-sandboxed “embed” elements don't — Wix's drag-in Embed HTML box clips the bubble to its little frame. On Wix use Settings → Advanced → Custom Code instead.

data-* reference

AttributeMaps toNotes
data-server-urlserverUrlYour session endpoint. Required for auto-mount.
data-namenameName in the widget header. Default: “Assistant”.
data-charactercharacterPortrait image URL. Default: the Bubblio mark.
data-primary-colortheme.primaryColorButtons and accents. Default: #1b77fd.
data-accent-colortheme.accentColorSecondary accent.
data-dot-colortheme.dotColorThe floating bubble. Defaults to the primary color.
data-trigger-timetriggers.timeOnPageSecNudge after N seconds on the page. The session starts only if the visitor clicks — a nudge never spends minutes.
data-trigger-exittriggers.exitIntentNudge on exit intent (cursor leaves the top; desktop only).
data-trigger-pathtriggers.pathMatchOnly nudge on matching paths — * wildcards, e.g. /pricing/*.
data-trigger-messagetriggers.messageThe nudge copy. Dismissals stick for the browser session; max 2 nudges per visit.

After the script loads there's a runtime API for single-page flows: window.Bubblio.init(config) / Bubblio.destroy(). Call it from the script tag's load event or a user interaction — never from an inline script above the tag (it hasn't loaded yet there; that's what window.bubblioConfig is for).

Tools: answers from your own data

declare + answer a toolts
// 1 · Declare the tool on the session (session route):
const session = await createBubblioSession({
  bubblioApiKey: process.env.BUBBLIO_API_KEY!,
  personality: 'You are Aria…',
  tools: [{
    name: 'get_order_status',
    description: 'Look up an order by its id',
    parameters: [{ name: 'id', type: 'string', description: 'The order id', required: true }],
    callbackUrl: 'https://your-site.com/api/bubblio/tools',
  }],
})

// 2 · Answer it (app/api/bubblio/tools/route.ts) — signature verification is
// automatic; the secret is fetched via the API key you already have:
import { createBubblioToolRoute } from '@bubblio/server'

export const POST = createBubblioToolRoute({
  bubblioApiKey: process.env.BUBBLIO_API_KEY!,
  tools: {
    get_order_status: async ({ id }) => {
      // look it up in YOUR database, return any JSON
      return { status: 'shipped' }
    },
  },
})

Tool results go to the model — it reads them and answers; they are never rendered directly. To put a clickable card on screen, have the persona call the built-in show_link_card tool (every session also has set_animation_state for the bubble's ambient animation). Tools marked dangerous: true get platform-side guards — dedup, cooldowns, per-session caps — with zero idempotency code on your side. The same callback route later serves your agent door.

Email: the same teammate reads your inbox

Turn the email channel on in dashboard → Settings and Bubblio mints a private forwarding address. Point your support inbox at it (in Gmail: Settings → Forwarding → Add a forwarding address) and every customer email lands in Needs you with a reply already drafted from the same knowledge and playbook as the widget. Nothing is sent until you send it — the teammate drafts, you send, and you're copied on every reply that goes out. Replies come from your business name at that address, so the thread stays in one place. Newsletters, bounces, and out-of-office replies are filtered — they never become conversations and never count toward your monthly total. Chat conversations where the visitor left before your reply fall back to email the same way.

Troubleshooting

  • The bubble doesn't appear. Check data-server-url is set — without any configuration the embed waits silently, so an empty console isn't a clue. Once configured, [Bubblio] console messages point at anything else. A strict CSP needs cdn.jsdelivr.net scripts and inline styles allowed.
  • The panel opens but shows an error and Retry. That's your session endpoint failing — the widget surfaces the JSON error from any non-200. curl -X POST it directly; on Webflow/Wix the usual cause is the missing CORS header above.
  • window.Bubblio.init() throws “undefined”. Inline scripts run before defer'd ones — use window.bubblioConfig, or call init from the tag's load event.
  • Will it clash with my page's JS or CSS? No — everything is bundled and scoped inside the one script; it doesn't touch your page's React, if any.