Browser SDKCopy link to this section
The Floe browser SDK injects the AI overlay into your site or product. One script tag, one function call. No build step, no dependencies. The overlay renders on top of your existing UI inside its own container, handling voice AI conversation, Q&A, and guided actions without changing anything about your product.
InstallationCopy link to this section
Load the SDK, then initialize Floe from your own external script. Both tags are deferred — that is what guarantees Floe() runs after the SDK has loaded, so keep defer on both. Nothing is inlined, so this works under a strict Content-Security-Policy. Add this just before the closing </body> tag:
<!-- 1. Load the SDK (allow https://cdn.floe.so in your script-src) -->
<script src="https://cdn.floe.so/floe-sdk.iife.js" defer></script>
<!-- 2. Initialize from your own script -->
<script src="/floe-init.js" defer></script>
// floe-init.js — served from your own origin
const floe = Floe({
clientKey: "YOUR_CLIENT_KEY",
demoMode: true,
demoSiteId: "YOUR_SITE_ID",
});
floe.ready.then(() => console.log("Floe ready"));
// later: floe.disconnect();
Replace YOUR_CLIENT_KEY and YOUR_SITE_ID with the values from your site settings.
Floe() returns an SDK instance with a ready promise and a disconnect() method to tear the overlay down (shown above).
Only one Floe instance can be active per page. A second Floe() call throws, so wrap it in try/catch and call disconnect() on the first instance before starting another. This is the usual React Strict Mode failure.
Integrate with a coding agentCopy link to this section
Paste the following prompt into your coding agent from the root of your web project. Tell it which Floe agent you want, or point it to the matching setup guide below.
Integrate the Floe browser SDK into this web app.
Requirements:
- Read https://floe.so/docs/sdk and the matching setup guide before editing:
Demo: https://floe.so/docs/demo-agent/setup
Website: https://floe.so/docs/website-agent/setup
Support: https://floe.so/docs/support
Onboarding: https://floe.so/docs/onboarding
- Load https://cdn.floe.so/floe-sdk.iife.js once, after the app is interactive.
- Initialize window.Floe(...) exactly once in the browser with the configuration
for the selected agent. Do not initialize during server rendering.
- Store the returned instance, handle instance.ready, and call
instance.disconnect() during teardown or logout.
- Guard against duplicate initialization, including React Strict Mode remounts.
- Read the client key from this app's public/browser environment configuration.
A Floe client key is domain-bound and safe for browser use. Never expose a
secret Floe API key or put access tokens in userInfo.metadata.
- Follow this repository's framework conventions and CSP policy. Use the CDN
script shown in Floe's docs; do not add an npm SDK dependency.
- Add a small verification note or test for script loading, single
initialization, and cleanup.
Before editing, inspect the app's framework, root layout, authentication lifecycle,
and existing third-party script pattern. Then implement the smallest idiomatic change
and summarize the files changed and how to verify it.
The agent-specific setup pages include ready-to-paste prompts and configurations. The rest of this page is the shared API reference.
Choosing a modeCopy link to this section
One embed runs one mode. Pick it with a single flag:
- Demo mode (
demoMode: true) — the live, voice-led demo agent. Drives your real product and streams it to the visitor. Pair withdemoSiteId. - Website agent (
websiteAgent: true) — the top-of-funnel Ask Floe Q&A launcher with contextual nudges, which can hand off in place to a live demo. Floe resolves the site fromclientKey; do not adddemoSiteIdfor this mode.
demoMode and websiteAgent are mutually exclusive. If both are set, demoMode wins. With neither set, the SDK mounts the in-product overlay used for onboarding and support.
Use the same two-script embed as above and set websiteAgent: true in your init script:
// floe-init.js — website agent (top-of-funnel Q&A that hands off to a demo)
Floe({ clientKey: "YOUR_CLIENT_KEY", websiteAgent: true });
Onboarding or support: who starts the conversationCopy link to this section
The in-product overlay (neither flag set) covers two jobs with different opening postures. Both render collapsed, and neither connects a voice or text session until the user clicks the launcher. activation picks the default nudge and opening:
activation: "proactive"(default) — the onboarding agent. Its setup-oriented nudge appears automatically for eligible users; clicking the launcher starts the session with onboarding-oriented suggestions. Use this right after signup.activation: "on_demand"— the support agent. The agent stays dormant: no prompt, no automatic nudge, and no session started until the user opens the launcher. When they do, it opens by asking what they need. Use this everywhere else in your product. The nudge is off by default here rather than unavailable — seenudgein the reference below to turn it back on.
// floe-init.js — support agent (in-product, user-initiated)
Floe({ clientKey: "YOUR_CLIENT_KEY", activation: "on_demand" });
on_demand changes the opening posture, not what the agent can do. Ask it to walk you through creating a report and it runs the same guided workflow the onboarding agent would.
Because the difference is per user rather than per surface, most products set it from their own signup state in one embed:
Floe({
clientKey: "YOUR_CLIENT_KEY",
activation: user.isNewlySignedUp ? "proactive" : "on_demand",
});
activation is ignored in demo and website-agent mode.
What the SDK doesCopy link to this section
Once mounted, the SDK:
- Renders the overlay into its own container on
document.body. The website agent renders inside a shadow root, so your selectors don't match its internals and its styles don't leak out (inherited properties and CSS custom properties still cross the boundary). The demo and in-product agents render in the normal document. - Connects a voice session only after the visitor starts one. In the website agent the session starts with the visitor's microphone muted. See Website Agent: Type or talk.
- Streams the demo in demo and website-agent mode: the agent drives your product in a browser on Floe's servers and the visitor watches it live, as video, inside the overlay.
- Captures screen context for the in-product agent so it understands what the user is looking at, during active sessions only. See Security.
The SDK does not intercept your network requests, and it does not touch your application's own content. It makes a few changes outside its overlay:
- At startup it adds a font
<link>and a stylesheet for its own cursor and highlight styles todocument.head. - While an in-product or website-agent session is connected, it wraps
history.pushStateandhistory.replaceStateto notice route changes. - In docked-sidebar mode it sets
margin-rightandmax-widthon<html>to make room for the sidebar, and marks the element withdata-floe-docked— see Website Agent Setup. - A full-screen demo temporarily locks background scrolling with inline styles on
<body>and<html>, restored when it closes. - The in-product agent clicks and fills elements on your page when it guides a user. That is the product working, not a side effect.
Configuration referenceCopy link to this section
Everything is passed to Floe() as a single config object.
| Option | Type | Description |
|---|---|---|
clientKey | string | Required. Your site's client key, from site settings. |
demoMode | boolean | Run the live demo agent. Default false. |
websiteAgent | boolean | Run the top-of-funnel Ask Floe Q&A launcher. Default false. Ignored if demoMode is also set. |
activation | "proactive" | "on_demand" | In-product overlay only. Both wait for a launcher click before connecting. proactive (default) auto-shows an onboarding nudge and uses a setup-oriented opening; on_demand shows no automatic nudge and opens by asking what the user needs. Ignored in demo and website-agent mode. An unrecognized value falls back to proactive. |
mcp | object | Support (activation: "on_demand") only. Connect one authenticated remote MCP server with { serverUrl, getBearerToken, allowedTools }. An invalid configuration disables MCP for the session; support still works without your tools. See Support: Live account data with MCP. This is Floe calling your MCP server; connecting your own AI tool to Floe is a different feature — see Connect your AI tools to Floe. |
consentMode | "not-required" | "pending" | Gate identity and visitor analytics behind your own consent UI. When omitted, your site's configured default applies, and not-required when no site default is set; that site default has no dashboard control today, so ask us if you want it changed. pending keeps the agent usable but defers browser identifiers, configured userInfo, identity capture, visitor/nudge events, and conversation continuity until consent("granted"). See Consent gating. |
launcher | object | Closed-launcher placement for website, onboarding, and support agents: { position?: "bottom-right" | "bottom-left", inset?: number }. Defaults to bottom-right; inset is pixels from the viewport edge. |
exitIntent | true | object | Website agent or docked demo only. Opt in with true, or customize { enabled?, message?, minTimeOnPageMs?, thresholdPx? }. Defaults to a 5-second dwell and a 20px top-edge threshold; out-of-range values are clamped. Needs a real mouse, so it never fires on touch devices. See Exit intent. |
demoSiteId | string | Demo mode only. The ID of the site to demo. The website agent resolves its site from clientKey. |
embedMode | "docked" | "fullscreen" | How the demo renders. docked (default) starts as a bottom-docked pill and goes full-bleed once the demo starts; fullscreen opens the demo immediately. Use fullscreen on dedicated demo pages. |
enableAudio | boolean | Whether the visitor's microphone starts enabled. Default true. It does not control whether the agent speaks — that's the speaker toggle in the UI, or toggleSpeakerMute(). Ignored by the website agent, which always starts with the mic muted. |
enableScreenCapture | boolean | In-product onboarding/support only. Share active-page context so the agent can guide the live UI. Default true; demo mode disables it. |
nudge | object | Onboarding/support launcher nudge: { text?: string, autoShow?: boolean, autoHideDelay?: number }. autoShow defaults to true for proactive and false for on_demand. autoHideDelay is milliseconds and 0 keeps it visible. Website contextual nudges are configured in the dashboard, not here. |
userInfo | object | Current-session context: { externalId?, email?, phone?, name?, company?, designation?, metadata? }. Supply phone in international format with its country code. Floe uses it to personalize the session and to skip identity asks for details it already matches; a value passed this way suppresses that ask only, and is not recorded as a contact detail captured from the visitor. It is unverified browser input: it never authenticates the visitor, never verifies that a detail belongs to them, and is never used as identity. Use metadata only for non-secret context. |
prospectEmail | string | Pre-fill the prospect's work email so the intro form skips the email step. Pre-filling never starts the session on its own. |
websiteAgentDisplayMode | "floating" | "sidebar" | Website agent only. Override the dashboard display setting for testing or staged rollout. |
websiteAgentSidebarWidth | number | Website agent sidebar width in pixels. Clamped to 320–480; default 400. |
avatarIdleAfterMs | number | Video avatar only. Off by default (0): the live presenter stays up for the whole call. Set a number of milliseconds of agent silence after which the presenter hands over to its resting clip; the live stream returns as soon as the agent speaks again. Minimum 3000. It does not change what the avatar costs. |
demoCalendarLink | string | Optional HTTPS booking URL override for both agents. Falls back to the site's booking URL. A complete {email} query value opts into email prefill; ordinary URLs receive no visitor details. Connected scheduling pages may receive an anonymous attribution parameter. See booking setup. |
demoLinkId | string | Demo mode only. The ID of the demo link this session came from, so per-link settings — persona, discovery, identity capture — apply. The hosted demo-link page sets this for you; you only need it when you host your own demo page. |
demoIdentityCapture | object | Demo mode only. Override the intro-form policy. See Demo identity capture. |
skipOnboardingModal | boolean | In-product only. Treat the user as returning rather than new, which changes the launcher's greeting. |
apiUrl | string | Override the Floe API URL. Defaults to production. |
debug | boolean | Verbose console logging. Default false. |
Identity and browser continuityCopy link to this section
userInfo arrives from browser JavaScript, including inside an authenticated application. An externalId or email in it does not authenticate the person, and Floe never uses it to look up activity from another browser.
When visitor collection is enabled and consent permits it, Floe stores a first-party identifier in the visitor's browser so it can resume that browser's active conversation and greet a returning visitor naturally. Clearing site storage or switching browsers starts fresh. An email or phone a visitor shares is attached to that conversation or demo session only. See Security & Privacy.
Demo identity captureCopy link to this section
Configure conversational timing and required details in the dashboard or on the demo link. demoIdentityCapture supplies the matching policy to the embedded start form when you host your own demo page; the hosted demo-link page supplies it automatically.
const floe = Floe({
clientKey: "YOUR_CLIENT_KEY",
demoMode: true,
demoSiteId: "YOUR_SITE_ID",
userInfo: { phone: "+12025550123" },
demoIdentityCapture: {
enabled: true,
timing: "before_demo",
fields: ["email", "name"],
askFields: ["name", "phone", "email"],
requiredFields: ["phone"],
phoneRegion: "US",
},
});
askFields is the full field selection: name, phone, email, company, and role. If omitted, the SDK reads fields. requiredFields may contain only selected fields and defaults to empty. The start form always requires email and name. phoneRegion is a two-letter country code matching the site's phone region.
Supported timings are before_demo, after_intro, after_first_feature, on_walkthrough_request, and never. The later timings show a Start control without an identity form. See Ask for their details for the conversational behavior.
Recap Schedule linksCopy link to this section
A Schedule a follow-up link in a recap email can land on a page where Floe is installed, carrying a single-purpose token in the URL fragment. Floe() removes it from the address bar on initialization, but scripts that run before the SDK can still read location.hash, so do not log or forward URL fragments on pages that receive recap links. If Floe cannot return to your page it uses a hosted fallback page with an explicit Continue to scheduling button. See What the recap actions do.
Launcher placementCopy link to this section
The closed launcher defaults to the bottom-right. Use bottom-left when that corner is occupied by another widget; inset optionally sets its pixel distance from the viewport edge. This applies to website, onboarding, and support agents. Demo mode has no closed launcher.
Floe({
clientKey: "YOUR_CLIENT_KEY",
websiteAgent: true,
launcher: { position: "bottom-left", inset: 24 },
});
Exit intentCopy link to this section
Exit intent gives a visitor one final invitation without pretending they chose to start a conversation. It is opt-in and works with the Website Agent or a docked Demo Agent:
const floe = Floe({
clientKey: "YOUR_CLIENT_KEY",
websiteAgent: true,
exitIntent: {
message: "Before you go — want to see it live?",
minTimeOnPageMs: 5000,
thresholdPx: 20,
},
});
exitIntent: true uses the same defaults shown above. An object enables the feature unless enabled: false. After the dwell time, a desktop pointer leaving the window near its top edge opens the dormant surface, at most once per SDK instance. Ordinary movement inside the page does not trigger it.
- The Website Agent opens its dormant panel with an exit-specific greeting.
- A docked Demo Agent expands its existing intro pill.
- Fullscreen demos and the onboarding/support overlay do not support exit intent.
Opening the surface is free and passive. It does not start a paid session, request microphone access, or send a visitor turn. The visitor must click or type before any of those actions can happen.
Runtime and custom signalsCopy link to this section
Use the instance methods when consent, targeting, or the abandonment signal is owned by your application:
enableExitIntent({ message?, minTimeOnPageMs?, thresholdPx? })disableExitIntent()showExitIntent({ message? })
await floe.ready;
// Enable or replace automatic desktop detection at runtime.
floe.enableExitIntent({ minTimeOnPageMs: 8000, thresholdPx: 16 });
// A customer-owned mobile or funnel signal can show the same passive surface.
checkoutFlow.onAbandonment(() => {
const shown = floe.showExitIntent({ message: "Need help before you go?" });
if (!shown) showExistingFallback();
});
// Stops automatic detection; manual showExitIntent() remains available.
floe.disableExitIntent();
enableExitIntent(options?) returns false only when the selected agent mode cannot provide an exit-intent surface. Calling it before ready is safe. showExitIntent(options?) returns true only when an eligible mounted surface claims the request; it returns false before readiness, after the visitor has engaged, or once the one-shot invitation has already appeared — automatic and manual triggers share that one allowance. When a manual call omits message, it reuses the configured one.
EventsCopy link to this section
Floe() returns an instance that emits events. Subscribe with on(event, handler).
const floe = Floe({ clientKey: "YOUR_CLIENT_KEY", websiteAgent: true });
floe.on("ready", () => console.log("overlay mounted"));
floe.on("connected", () => console.log("session started"));
floe.on("demoEnded", ({ summary, featuresShown, nextStep }) => {
console.log(summary, featuresShown, nextStep);
});
Available in every mode
| Event | Payload | Fires when |
|---|---|---|
ready | — | The overlay has mounted. Note that this does not fire if your client key is rejected or the agent is disabled for the site, even though Floe() itself returns normally. Use this event, not the instance's ready promise, as proof the overlay is live. |
connected | — | A session has started. This is the moment billing begins. |
disconnected | — | The session ended. May fire more than once for a single teardown. |
closed | — | The SDK was torn down via disconnect(). Distinct from disconnected, which is about the call. |
botReady | — | The agent is initialized and ready to talk. |
userSpeaking | boolean | The visitor started or stopped speaking. |
botSpeaking | boolean | The agent started or stopped speaking. |
userTranscript | { text, final } | A speech-to-text result for the visitor. Fires for interim results too, so expect several per spoken sentence. There is no matching event for the agent's words. |
languageChanged | { language, source, changed } | The session switched language. See Languages & Voices. |
error | varies | Something failed. The payload is either a thrown Error or an object with a type discriminator — feature-detect payload?.type before reading it. |
Demo and website agent
| Event | Payload | Fires when |
|---|---|---|
demoEnded | { summary, featuresShown, nextStep } | The demo finished and the agent produced its wrap-up. |
identityAsk | { askId, field, status, value?, displayValue?, outcome?, mandatory, confirmRequired, revision } | A conversational contact ask is shown (status: "asked"), proposed for confirmation ("proposed", with the normalized value), or resolved ("resolved", with outcome confirmed or declined). field is name, phone, email, company, or role. Proposed events contain personal contact data, so do not forward the payload to general analytics. |
exitIntentShown | { agentMode, customMessage } | The exit-intent surface was revealed. |
exitIntentEngaged | { agentMode } | A session started after exit intent had been shown. |
calendarRequested | { requestId } | The agent offered the booking calendar. Website agent only, and only when a booking URL is configured. |
bookingCompletedHint | { provider: "calendly" } | The Calendly booking page embedded by Floe reported that its flow completed. A UX hint only, not verified conversion evidence — use the provider-verified Booked call data for analytics or CRM logic. Not emitted for Cal.com or link-only booking. |
In-product agent (onboarding and support)
| Event | Payload | Fires when |
|---|---|---|
expanded / minimized | — | The user opened or collapsed the overlay. |
sessionPaused | server payload | The session was paused for inactivity and can be resumed. |
sessionStopped | — | The session was stopped via stopSession(). |
planStarted | { planName, totalSteps } | A guided workflow began. |
planProgress | { planName, currentStep, totalSteps, progressPercent, currentTaskTitle } | The user advanced a step. |
planComplete | { planName } | The workflow finished successfully. Stopping or abandoning a workflow does not emit this event. |
elementHighlighted | { target, description } | The agent highlighted an element on your page. |
Instance methodsCopy link to this section
| Method | Returns | What it does |
|---|---|---|
requestDemo(options?) | Promise<boolean> | Opens the agent already on the page, from your own button. Resolves false if no Floe surface handled it. See Opening the demo from your own button. |
disconnect() | Promise<void> | Ends any live session and unmounts the overlay. |
getStatus() | { initialized, connected, sessionId, microphoneMuted } | Current state. microphoneMuted reads false before a session exists. |
getSessionId() | string | The SDK's per-start correlation ID sent when a session starts — not the dashboard's session record ID. Each start or restart gets a fresh ID; disconnect() leaves the last ID readable until another session starts. |
sendTextMessage(text) | Promise<void> | Sends a message as if the user typed it. Queued if the agent isn't ready yet. |
toggleMute() | boolean | Toggles the visitor's microphone. Returns the resulting state. |
toggleSpeakerMute() | boolean | Toggles whether the agent speaks aloud. While a live demo is on screen the agent's captions keep updating. |
enableExitIntent(options?) | boolean | Arms exit-intent detection at runtime. |
disableExitIntent() | void | Disarms the detector. showExitIntent() still works afterwards. |
showExitIntent(options?) | boolean | Shows the exit-intent surface immediately, from your own signal. Never connects or touches the microphone. |
consent(decision) | void | With consentMode: "pending", "granted" begins identity and analytics collection. "denied" stops collection in either consent mode and is sticky for the current page load. |
Consent gatingCopy link to this section
Use consentMode: "pending" when your application already owns the consent banner and Floe must wait for its decision:
const floe = Floe({
clientKey: "YOUR_CLIENT_KEY",
websiteAgent: true,
consentMode: "pending",
});
cookieBanner.onAccept(() => floe.consent("granted"));
cookieBanner.onReject(() => floe.consent("denied"));
consent("granted") changes behavior only while consentMode is "pending". consent("denied") is honored in both modes.
Before grant, the Floe surface still renders and a visitor can keep chatting, but Floe does not associate activity with a browser identity, attach configured userInfo, collect visitor or nudge analytics, retain a Website Agent conversation, or ask for contact details in conversation. The one exception is the Demo Agent's start form: details the visitor deliberately types into that form are used to start the demo they requested, even while consent is pending or denied.
Grant begins identity and analytics collection from that point forward. A Website Agent conversation already running before the grant stays unretained; the next one can be retained. Earlier activity is never added later.
Denial stops identity and analytics collection for that SDK instance and is sticky for the current page load, so a later consent("granted") is ignored. A conversation already being recorded keeps recording until it ends; later conversations in that page load start unretained. Denial is not a deletion request; use your normal data deletion process for data already sent.
React usageCopy link to this section
Initialize Floe from the script's onLoad signal, and disconnect on unmount:
import Script from "next/script";
import { useEffect, useRef } from "react";
export function FloeDemo() {
const floeRef = useRef<{ disconnect?: () => void } | null>(null);
// Tear the overlay down when the component unmounts.
useEffect(() => () => floeRef.current?.disconnect?.(), []);
return (
<Script
src="https://cdn.floe.so/floe-sdk.iife.js"
strategy="afterInteractive"
onLoad={() => {
if (floeRef.current) return; // guard against double-init
floeRef.current = (window as any).Floe({
clientKey: "YOUR_CLIENT_KEY",
demoMode: true,
demoSiteId: "YOUR_SITE_ID",
embedMode: "docked",
});
}}
/>
);
}
onLoad fires once window.Floe is available, so it's the safe place to initialize.
Opening the demo from your own buttonCopy link to this section
Your page probably has its own "See it live" call-to-action — in the hero, the nav, the footer. Those sit outside the agent, so they need a way to drive it.
Call requestDemo() on the instance Floe() returned. It surfaces the agent already on the page rather than launching a second one, and it works the same whichever agent you embedded:
- Website agent — opens the panel with the demo request already sent, so the visitor lands mid-conversation instead of on an empty chat.
- Demo agent — expands and focuses the docked pill.
// floe-init.js
const floe = Floe({
clientKey: "YOUR_CLIENT_KEY",
websiteAgent: true,
});
document.querySelector("#see-it-live").addEventListener("click", async () => {
if (!(await floe.requestDemo())) {
// Nothing took the request — fall back to whatever you'd do without Floe.
window.open("/book-a-call", "_blank");
}
});
Check the result. requestDemo() resolves true only if an agent actually took the request. It resolves false when the SDK never booted, when your site has no demo available yet, or when the agent you embedded isn't enabled for your site — all cases where a button that appears to work would do nothing at all. Give the visitor somewhere else to go.
It waits briefly before answering (1500ms by default, override with { timeoutMs }), so a click that lands while the agent is still mounting is still handled. Keep that budget under about five seconds if your fallback opens a new tab — browsers stop treating the click as user-initiated after that and will block the popup.
For the in-product onboarding/support agent there is no demo to open, so requestDemo() returns false immediately.
Verifying the installationCopy link to this section
- Open your product in Chrome.
- Open DevTools (F12) → Console.
- Type
window.Floeand press Enter. You should see a function. - Confirm the overlay (demo pill or Ask Floe launcher) appears on the page.
If window.Floe is undefined, check that the script tag is present in the page source and loaded without error. If it's defined but nothing appears, check that clientKey is correct and matches your site settings.
Framework compatibilityCopy link to this section
The SDK works with any web framework or none — React, Vue, Angular, Svelte, plain HTML. Because it's a script tag plus a function call, and mounts its own container on document.body, it sits outside your application's component tree entirely.
FAQCopy link to this section
Does the SDK work on single-page applications? Yes. It detects route changes automatically and re-scopes the agent's context — including the website agent's page-scoped nudges — as the user navigates within your SPA.
Will the SDK slow down my product? No. It loads asynchronously and does not block rendering.
Can I run demo and website-agent mode from one embed?
No — one embed runs one mode. demoMode and websiteAgent are mutually exclusive (demoMode wins if both are set). Use the mode that fits the surface.
Can I use different keys in development and production? Yes. Use your development client key in dev and the production key in prod. Demo mode also needs the matching site ID. The dashboard shows each site's sessions separately.
What if I need to remove the SDK later?
Call disconnect() on the instance returned by Floe() — that unmounts the overlay and ends any live session. Removing the script tag alone does not tear down an overlay that has already mounted. disconnect() does not clear browser storage: the website agent's first-party visitor identifier stays in local storage until the visitor clears site data.