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
npm install @bubblio/server @bubblio/widgetGrab 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.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.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.
<!-- 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><!-- 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:
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' },
})
}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
| Attribute | Maps to | Notes |
|---|---|---|
data-server-url | serverUrl | Your session endpoint. Required for auto-mount. |
data-name | name | Name in the widget header. Default: “Assistant”. |
data-character | character | Portrait image URL. Default: the Bubblio mark. |
data-primary-color | theme.primaryColor | Buttons and accents. Default: #1b77fd. |
data-accent-color | theme.accentColor | Secondary accent. |
data-dot-color | theme.dotColor | The floating bubble. Defaults to the primary color. |
data-trigger-time | triggers.timeOnPageSec | Nudge after N seconds on the page. The session starts only if the visitor clicks — a nudge never spends minutes. |
data-trigger-exit | triggers.exitIntent | Nudge on exit intent (cursor leaves the top; desktop only). |
data-trigger-path | triggers.pathMatch | Only nudge on matching paths — * wildcards, e.g. /pricing/*. |
data-trigger-message | triggers.message | The 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
// 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-urlis 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 needscdn.jsdelivr.netscripts and inline styles allowed. - The panel opens but shows an error and Retry. That's your session endpoint failing — the widget surfaces the JSON
errorfrom any non-200.curl -X POSTit 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 — usewindow.bubblioConfig, or call init from the tag'sloadevent.- 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.