Chat on your website
How to put an agent's chat window on your own website, or share it as a link, and how to style it and work with it from your page.
Overview
The chat window lets anyone talk to an agent without signing in to anything. Put it on a marketing site, a help center, a client portal, or any page where you can add HTML, or send people a link to a page of its own.
Out of the box, the chat window handles streaming responses, Markdown, file attachments, file downloads, answers to tap under a reply, dark mode, and reconnecting when the connection drops.
In Salesforce, the same chat window is the Chatbot component. See Chatbot on Salesforce pages. For an agent that answers through a page of its own rather than a chat window, see Web apps.
Adding it to your website
Prerequisites
- A deployed agent you own. The snippet carries its Agent ID.
- The agent open to the public. A website's visitors are not signed in, so the Agent Platform admits them only to an agent opened to the public, and only once that agent has a monthly spending limit of its own. Each visitor chats as an anonymous visitor who can reach this agent and nothing else, and every conversation is billed to the agent's owner and counts against its limit.
Generating the Snippet
- Open the agent in the builder and go to Sharing.
- Under Public access, click Open to anyone on the web. It needs a deployed version.
- If the agent has no monthly limit yet, enter a Monthly limit (USD). It is the same limit as the agent's Cost settings. Click Open it.
- Click Copy website snippet and paste the snippet into your website's HTML.
The snippet it copies is for Bubble mode, a floating chat button in the corner of the page. For Inline mode, or to change the button's color or label, edit the attributes on the tag (below).
Close public access, in the same place, turns it off. The link and every website snippet stop working at once, and anyone in the middle of a conversation can't send another message.
The snippet is a single script tag. All of the widget's logic lives in a centrally hosted script, so fixes and improvements reach your site automatically and you never need to paste it again.
From Salesforce: the Agent Builder's Embed panel opens the agent to the public and writes the snippet, too, with a choice of mode, an accent color, and a test mode switch. See Embed.
Sharing a link instead
Once the agent is open to the public, Copy link, beside Copy website snippet, copies a link to a page of its own where anyone can chat with the agent. Use it where you cannot add HTML, such as an email or a social post. It is open on the same terms as the snippet.
For a few named people rather than everyone, click Create a preview link under Preview links in the same section instead. A preview link lets whoever holds it chat with the deployed version without signing in until it expires after the number of days you choose (up to 30). Every conversation is billed to the agent's owner. The link is shown once, when you create it, so copy it then. Revoke stops it at once.
Where to Paste the Snippet
Bubble mode: Paste the tag just before the closing </body> tag. It creates the launcher and the chat panel itself. No container element is needed.
<!-- Flourish Chatbot -->
<script src="https://cdn.toflourish.org/agent-platform/v1/chatbot-embed.js" defer
data-agent-id="YOUR_AGENT_ID"
data-mode="bubble"
data-accent="#2A809C"></script>
Inline mode: Paste the snippet wherever you want the chat window to appear. It builds its own container there, and data-height and data-max-width set its size.
<!-- Flourish Chatbot (inline) -->
<script src="https://cdn.toflourish.org/agent-platform/v1/chatbot-embed.js" defer
data-agent-id="YOUR_AGENT_ID"
data-mode="inline"
data-accent="#2A809C"
data-height="600"
data-max-width="480"></script>
Snippet attributes
They are the loader's contract, so the spellings matter.
| Attribute | Default | What it does |
|---|---|---|
data-agent-id | - | Required. The agent to open. |
data-mode | bubble | bubble for the floating launcher, inline to sit in the page. |
data-accent | #2A809C | The launcher's color, and inside the chat the send button, links, and the visitor's own messages. |
data-position | right | left or right. Bubble mode only. |
data-label | Let's chat! | Text on the launcher button. |
data-title | Chat | The frame's name for screen readers. The title on the chat itself is the agent's Chat title. |
data-height | 640 | Inline mode only. Height in pixels, or a CSS length such as 100% inside a container you have sized. |
data-max-width | none | Inline mode only. Width cap in pixels. Leave it off to fill the space. |
data-session | - | host means your own server mints the session and hands it in, instead of the chat window minting one for itself. See Minting the session yourself. |
data-test | - | Hides the widget from everyone but you. See Test mode. |
data-flstyle | - | Styling JSON for the chat window, in place of the loader's own. off sends none, so the chat keeps its own look. See Customizing the Embed. |
data-embed-url | - | Legacy. Frames the page it names (a Salesforce Site page hosting the Chatbot) instead of Flourish's chat page. Snippets that carry it keep working. Leave it off new ones. See A Salesforce Site page in an iframe. |
There is no attribute for the interface language. An embedded chat window takes the agent's own Chat window language, which is the right answer for most sites. See Text and language.
Building the iframe yourself
The snippet is the simplest way in, but you can also frame Flourish's chat page directly. Put the agent ID on its URL as c__flagentid:
<iframe src="https://cdn.toflourish.org/agent-platform/v1/chat.html?c__flagentid=YOUR_AGENT_ID"
allow="clipboard-write" style="width:100%;height:600px;border:0"></iframe>
Things to know:
- The agent must be open to the public, exactly as for the snippet.
- Use the platform Agent ID. See Finding an agent's ID. A Salesforce record ID means nothing to an external site.
- Treat the ID as public. It is visible in your page source, and anyone can change it in the URL. That is safe because only agents opened to the public answer, each visitor reaches only that one agent, and every conversation counts against its limit.
- Add
c__flparentorigin(your page's origin, URL-encoded) to use the Host API from your page. Without it the chat ignores messages from the parent.
The chat page reads these parameters, each also without the c__ prefix:
| Parameter | Purpose |
|---|---|
c__flagentid | Which agent to load. Set automatically from data-agent-id when you use the loader. |
c__flstyle | URL-encoded JSON using the Custom Styling contract. |
c__flparentorigin | Your page's origin, needed for the Host API. |
c__fllang | An interface language for this placement, overriding the agent's own. |
c__flcontext | URL-encoded JSON the agent reads as background from the first message. fl__setContext replaces it later. |
c__flconversationid | A conversation to reopen, from fl__sessionready. It reopens only in the browser tab that started it. |
Test Mode
Add data-test="1" to the snippet and the chat window stays hidden from visitors. It only activates for someone who opens a page with ?testflchatbot=1 in the URL, and it then stays active as they navigate around the site for the rest of their tab session. ?testflchatbot=0 turns it off again.
This lets you put the snippet on your production website and test it end-to-end before anyone else can see it. To launch for everyone, remove the data-test attribute.
Bubble Mode Behavior
The bubble embed puts two things on your page:
- A launcher, a rounded button in the accent color with a chat icon and the
data-labeltext. It sits in the bottom-right corner, or the bottom left withdata-position="left". Clicking it opens and closes the chat panel. - A chat panel, 400 by 640 pixels, which opens above the launcher. Its X button, or Escape, closes it.
On a screen narrower than 520 pixels or shorter than 700 pixels, the panel fills the screen.
In both modes, a short footer under the chat reads This is AI and can make mistakes, with a Terms link to Flourish's terms and a Powered by Flourish line.
Inline Mode Behavior
The inline embed places the chat window where the script tag sits. It is as tall as data-height (640 pixels unless you set it) and fills the width of the space it is in up to data-max-width if you set one. It does not grow with the conversation. The messages scroll inside the frame, so pick a height that suits the page rather than the longest conversation.
Keeping the Conversation Across Pages
Every link a visitor clicks on your site is a fresh page load, which destroys the chat frame. The loader hands the conversation to the next page so the thread survives, and a bubble panel that was open comes back up without stealing focus.
There is nothing to configure. Two things are worth knowing about where it stops:
- It ends with the browser tab or after 30 idle minutes. That is deliberate. A conversation someone had about their health, their finances, or their employer must not silently reopen for the next person on a shared computer.
- It degrades to a fresh conversation, never to an error, if the visitor's browser blocks storage.
A conversation reopens only in the browser that had it. The chat page keeps its session in that tab's storage, and the Agent Platform resumes a visitor's conversation only with the session it was started under, so a conversation id on its own opens nothing.
For a "start over" control of your own, call FlourishChatbot.reset(). It ends the thread and starts a new one in place.
Customizing the Embed
Changing the color: Edit the data-accent attribute on the tag.
Changing the inline size: Adjust data-height and data-max-width on the tag.
Passing custom styles to the chat window: Put the JSON on data-flstyle as it is, in single quotes, using the Custom Styling contract. The loader encodes it for you. For example, to keep the message area from collapsing: data-flstyle='{"layout":{"bodyMinHeight":"50vh"}}'.
It replaces the loader's own styling of the chat window as a whole. The chat's colors then no longer follow data-accent unless you set them in colors, and a second Powered by Flourish line appears inside the chat unless you add "show": { "poweredBy": false }.
Older embeds: Sites embedded with the previous self-contained snippet (a full inline script rather than a one-tag loader) keep working. They don't receive central updates. Copy the new snippet whenever convenient.
Writing code against an embed? The events the iframe exchanges with your page, the methods for driving the conversation from the host, and receiving agent actions are all covered under Iframe Integration.
Custom Styling
You can brand, resize, and slim down the chat window with a single JSON value (data-flstyle on the snippet, c__flstyle on a hand-built iframe). It has four sections:
colors: palette overridesshow: hide or show individual componentslayout: width, height, and shapetext: reword any built-in label
Colors
| Key | What it colors |
|---|---|
accent | The brand color: send button, links, activity spark, focus ring |
accentPress | Send button hover |
onAccent | The icon on the send button |
ground | Background behind the conversation |
surface | Header bar, composer surface, menus |
surface2 | Table headers, disabled send, subtle fills |
border / borderStrong | Hairlines / the composer's outer border |
textPrimary / textMuted / textFaint | Text hierarchy |
userBubble / userText | The user's message bubble |
focusRing | Glow around the composer when focused |
Any color is either a plain string (applies in both themes) or a { "light": …, "dark": … } pair. Dark values switch in automatically with the dark-mode toggle. There is no separate dark section.
Show / Hide Components
Set any of these to false under "show" to hide it: header, avatar, disclaimer, and poweredBy.
Layout
| Key | Purpose |
|---|---|
width, height, minHeight, maxHeight | Size of the whole chat frame. "fill" is shorthand for 100%. |
bodyHeight, bodyMinHeight, bodyMaxHeight | Size of the scrolling message area specifically. bodyMinHeight prevents the chat from collapsing when there are few messages. |
radius, headerRadius | Outer container / header corner radius (set to 0 for flush embeds) |
"fill"makes the chat window match its parent, which only works if the parent element has a size of its own. Width almost always does. Height often doesn't in a page region sized by its content, wherebodyMinHeightis the better tool.
Example:
{
"colors": {
"accent": { "light": "#7C3AED", "dark": "#8B5CF6" },
"userBubble": "#EDE9FE"
},
"show": { "poweredBy": false },
"layout": { "height": "fill", "bodyMinHeight": "50vh" }
}
Text and language
The chat window's own labels, such as the message box's placeholder, the file chips, and the connection notices, come in English and Spanish. Set the language to a locale code such as es-us and they all switch together. Any other language shows them in English, and text (below) can reword them in that language.
Use the full code, with the region. A bare code like es works, but store and pass es-us. It is unambiguous, and it leaves room for wording that differs between Spain and the US.
You don't always have to set it. The chat window takes the c__fllang URL parameter when a page passes one, and otherwise the agent's own Chat window language from the builder's Basics. So the agent decides unless the page says otherwise. An agent set to Follow the page shows English here, because the chat window can't read the language of the page around it.
A page serving more than one language has to say which one it wants, per placement. A hand-built iframe passes
c__fllang, and a link takes?lang=and the code, such as?lang=es-us. The snippet has no attribute for it.
Language does not change what the agent says. It translates the chat window's labels, not the replies. The reply language is part of the agent's own configuration, so setting the language to Spanish gives you Spanish buttons around an English answer unless the agent is set up to answer in Spanish, too.
To reword a label, in any language or regardless of language, use text:
{
"text": {
"composerPlaceholder": "Ask about your benefits",
"reconnect": { "en": "Try again", "es": "Intentar de nuevo" }
}
}
A plain string applies in every language. An object picks by language code, matched on the full code first and then on the language. Anything you do not list keeps its built-in wording, and a key that is not one of the ones below is ignored.
The keys are composerPlaceholder, composerPlaceholderTurn, send, stop, attach, more, moreActions, voiceInput, removeAttachment, attachLimit, copyMessage, jumpToLatest, answers, download, uploading, uploadFailed, typePdf, typeCsv, typeDoc, typeAudio, typeVideo, typeText, typeFile, loading, preview, closePreview, thinking, working, stopping, stoppingHint, agentFallback, getInTouch, voiceStart, voiceStop, toggleDarkMode, suggestionsTitle, reconnect, reconnecting, connectionLostOffline, connectionLostFailed, superseded, contactInformation, reportAnIssue, contactPrompt, and legacyPlaceholder. answers and superseded are only in the chat window on your website.
attachLimit carries a {0} placeholder for the file count, so you can put the number where your wording wants it.
Right-to-left languages aren't offered yet, because the layout reads left to right throughout.
Dark Mode
The chat window has a built-in dark mode toggle in the header (a sun/moon icon). The visitor's preference is remembered in their browser and restored on the next visit.
Streaming Responses
The chat window streams responses. Words appear as the agent writes its reply, so nothing waits for the whole response.
While the agent works, an animated activity spark with a "Thinking…" label appears in the conversation. Internal details such as reasoning steps and tool names are not shown to visitors.
Stopping a reply
A Stop button appears while a response is being written.
Stopping is not instant. The agent finishes whatever step it is on before it stops, so an ordinary reply stops in a second or two and one running a long piece of code takes as long as that step takes. While it is stopping, the label changes to Stopping… and the button goes inactive, so pressing it again does nothing.
What Stop always does is drop anything waiting behind the reply. What it cannot always do is stop a sentence already being written. If the agent had started composing its answer, that answer finishes on the Agent Platform even though the chat stops showing it, so the words on screen stop where you stopped them.
Stopping is not a refund. Whatever the agent had already spent is spent.
Nothing is written into the conversation to say a reply was stopped.
Adding to a request while it is working
The box stays open while the agent is working, and the placeholder changes to Add to your request…. Send something and the agent takes it up, usually in the reply it is already writing.
Send appears beside Stop rather than replacing it. Stop abandons the work, and Send adds to it.
A message sent this way is inserted above the reply being written, not below it, because one answer covers both questions. If the agent was too far along to take it up, the message runs as the next question instead and gets its own answer.
The box does close for the few seconds after you press Stop. Anything typed then would be dropped with everything else in the queue, so the box doesn't take it.
Progress Notifications
While the agent works, it may send short status messages, called progress notifications, that appear in the chat. They show in light italic text, apart from the agent's answer, so you can see what the agent is doing before it answers.
Answers to tap
When the agent has Choices on in its Tools, it can put a few answers under its reply for the visitor to tap instead of typing. Each answer has a title and sometimes a line under it. A tap sends that answer as the visitor's own message, marks it, and closes the card. A card can also offer a question to tap, which sends its words without picking an answer.
Typing instead still works, and the card stays open until an answer is tapped. A reopened conversation shows the card and the answer picked.
Choices show in the chat window on your website and in Greenhouse. The Chatbot in Salesforce does not show them.
File Uploads
When Let people attach files is on in the agent's Basics, visitors see a paperclip button next to the message box. Files upload to the conversation, where the agent's code can read them.
How It Works
- Click the paperclip and select one or more files (up to 10 per message, 30 MB each).
- Selected files appear in a strip above the message box, images as thumbnails and other files as small cards.
- Type a message and click Send. The files upload first, and then the message is sent.
- While a file uploads, its card shows a spinner, and an image shows an overlay. Both clear when the upload completes.
If an upload fails, such as for a file that's too large or a dropped connection, the file's card shows an error and a message appears briefly. The message is still sent even if some uploads fail.
Pasting
You can paste into the message box as well as use the paperclip.
- A screenshot or any image, audio, video, document, or source file becomes an attachment as if you had picked it with the paperclip. Any file type works.
- A long block of text, over 2,000 characters, is attached as
pasted-text.txtinstead of filling the message box. The box only grows so far, and past that point you cannot read back what you pasted before sending it. Shorter text pastes into the box as normal. - Copying from a spreadsheet or a web page pastes the text, not a picture of it. Those copies put both on the clipboard, and the text is almost always what you meant.
Pasted files are named for you and numbered if you paste several, so three screenshots arrive as three attachments rather than overwriting each other.
Attachments show what kind of file they are, and that is worth reading before you send. The agent can look at images when Image Reading is on, reads text and code directly, and needs the right Python package to open a PDF or a Word document. It has no way to listen to audio or watch video today.
Pasting only attaches when the agent lets people attach files. Without that setting, pasting puts text in the box as usual.
File Downloads
When the agent delivers a file, a card appears in the chat.
Images show as a thumbnail with the filename and size, and Preview and Download beneath. Click the thumbnail or Preview to see the picture full size over the conversation. Close it with the X, by clicking outside it, or with Escape. PNG, JPEG, GIF, WebP, BMP, AVIF, and SVG all display.
Everything else shows as a download card with an icon, the filename, the size, and a Download button.
Clicking Download fetches a fresh download link and opens the file in a new tab. Generated files are kept for 180 days.
Delivered files do not reappear when a conversation is reopened. The agent's message about the file comes back, but the card doesn't. Download anything you want to keep before you close the conversation.
Markdown Rendering
The agent's replies are shown as formatted Markdown, including headings, bold and italic, code blocks, inline code, lists, tables, blockquotes, and links. Links always open in a new tab.
A copy button appears when you hover over a reply. It copies the reply's Markdown to your clipboard.
If the Markdown renderer fails to load, the chat window falls back to basic inline formatting so the chat keeps working.
Conversation History
When a visitor comes back to a page in the same browser tab within 30 idle minutes, the chat window reopens their conversation and replays its messages. They don't have to do anything. See Keeping the Conversation Across Pages.
Nothing on the chat window starts a new conversation. A page can offer its own control for that with FlourishChatbot.reset().
Connection & Reconnection
The chat window opens a live connection to the Agent Platform when it loads. If the connection drops, it reconnects on its own and the conversation carries on where it was.
It waits for signs the reader is back rather than counting down from the drop. While the tab is hidden or the computer is offline, no attempts are spent, so a laptop that slept for an hour does not come back to a failed chat. Returning to the tab, coming back online, or sending a message all start an attempt straight away, and a message sent while disconnected is held and delivered once the connection returns.
While it is trying, attempts back off from about a second to thirty seconds, for roughly two minutes in all. After that the chat window shows a Reconnect button. The conversation hasn't ended. Pressing the button or returning to the tab tries again, and the transcript is still there.
When the same conversation opens in another tab or window, this one steps back and says so, with Reconnect to continue it here.
Some errors (authentication failures, credential issues, or an agent that can't be loaded) can't be resolved by reconnecting. In those cases, the chat window shows an error message instead of retrying.
An error says what to do about it, not only what happened. Two of these read almost identically and want opposite responses, which is why the chat window adds its own sentence to the platform's:
| What went wrong | What the chat window tells the reader |
|---|---|
| Too many messages in one conversation, too quickly | Wait a minute or two. It clears on its own. |
| This agent reached its own monthly spend limit | Retrying cannot help. Raise the limit on the agent or wait for the month to reset. See Cost. |
| The organization reached its monthly spend limit | Retrying cannot help, and every other agent in the organization is refused too until someone raises the organization's limit or the month resets. |
| The agent is not authorized | It's a configuration problem, so retrying cannot help. |
| A reply is already being written | Wait for it to finish. |
For Developers
This section covers how your page talks to the chat window: the events it sends, driving the conversation from your page, minting the session on your own server, and receiving agent actions. Using the Chatbot component inside your own Salesforce components is on Chatbot on Salesforce pages.
Iframe Integration
When the chat window runs in an iframe on your site, your page talks to it over postMessage. The same Host API methods drive the conversation, and the same agent actions come back, as in the Chatbot component inside Salesforce.
postMessage Events
The hosted script (chatbot-embed.js) reads its configuration from the data-* attributes on its own script tag. It then loads Flourish's chat page in an iframe with the agent ID on its URL as c__flagentid. The chat page speaks the same postMessage protocol as the Chatbot component does in an iframe, so the events below are the same whichever one is framed.
The chat window posts these to your page:
| Event | Direction | Purpose |
|---|---|---|
fl__ready | Chat window → Parent | The chat window's DOM has loaded (fires early, before the chat session exists) |
fl__sessionready | Chat window → Parent | The chat session is live, so the Host API (send message / attach data) is now safe to call. Carries conversationId. Fires again on reconnect. |
fl__resize | Chat window → Parent | The chat window's content height changed. The loader doesn't use it, since an inline embed keeps the height you gave it. |
fl__notify | Chat window → Parent | The agent finished a reply. A page can use it to show that a reply came in while the panel was closed. |
fl__needsession | Chat window → Parent | Only under data-session="host". The chat window is ready to be handed a session. See Minting the session yourself. |
fl__action | Chat window → Parent | The agent invoked a Custom Action. Payload { action: 'fl__action', payload: { type, ...params } }. |
All messages are JSON-encoded and origin-checked for security. fl__action is posted only to the validated parent origin (never *), since action payloads carry data. If the parent origin can't be determined, the action is not forwarded.
Driving the Chatbot from the Parent Page (Host API)
Beyond receiving events, the parent page can drive an embedded chat window: send messages, hand the agent data, and show a busy state. This lets the host page act as an orchestrator. For example, run a local search, hand the results to the agent, and have it answer from them.
Because an iframe boundary separates the parent from the chat window, the parent cannot call its methods directly. Instead it posts JSON messages to the iframe. The chat window checks the sender's origin and runs the matching method. If you use the hosted chatbot-embed.js loader, a convenience wrapper is exposed as window.FlourishChatbot. If you host your own iframe, post the raw messages yourself.
Parent to chat window messages:
action | Payload | Effect |
|---|---|---|
fl__sendMessage | { text, origin? } | Sends a message. origin: 'host', the default for a raw message, delivers it as a state-change notification. It draws no bubble, and the agent decides whether to respond. origin: 'user' sends it as if the visitor typed it, with a bubble. |
fl__attachData | { name, data, contentType? } | Stages in-memory data (string, JSON-serializable object, or Blob) as a conversation upload. The agent's code reads it as uploads["<name>"], so the data never enters the model's context until the agent works with it. Up to 30 MB. |
fl__setHostBusy | { busy, label? } | Raises or clears the host's busy state (locks the composer, shows the activity indicator with label). The host controls only its own flag. It can never clear the agent's own "thinking" state, so it cannot falsely signal "ready for input" mid-response. |
fl__setContext | { context } | Replaces the ambient values the agent reasons over, such as language, who the visitor is, or what page they are on. Draws no bubble and provokes no reply on its own. It replaces the whole object rather than merging, so send every key you want kept, every time. |
fl__setsession | { session } | Only under data-session="host". Hands over a session your own server minted. Refused on an ordinary embed, so a page cannot point somebody else's chat at a conversation of its choosing. |
fl__attachData followed by fl__sendMessage is safe to fire back-to-back. The chat window runs them in order, so the upload completes before the message is sent.
Wait for fl__sessionready if you post raw messages, because anything that arrives before it is dropped. The loader's wrapper below queues instead, so calling it from DOMContentLoaded is safe. It holds up to 25 messages and sends them once the session is live. In bubble mode the frame is not built until the panel first opens, so a queued message waits for that rather than forcing the panel open.
The loader's wrapper. With the hosted chatbot-embed.js in place, window.FlourishChatbot carries all four methods plus a few of its own.
| Method | Purpose |
|---|---|
sendMessage(text, opts) | As above, except that the default here is origin: 'user', with a bubble. Pass { origin: 'host' }, or the bare string 'host', for a notification. |
attachData(name, data, contentType?) | As above. |
setHostBusy(busy, label?) | As above. |
setContext(object) | As above. Whole-object replace. |
setSession(session) | Hands the chat window a session your own server minted. Only does anything with data-session="host". See below. |
open() / close() / toggle() | Opens and closes the bubble panel. |
reset() | Forgets the conversation and starts a fresh one in place. |
terms() | Opens the Terms overlay the footer links to. |
debug() | Reports the frame address, the configuration read off the tag, and the current conversation ID. |
Everything the chat window posts is also relayed onto your page as a flourish: event on window, so you can listen without writing a postMessage handler. The name is the message name without its fl__ prefix.
window.addEventListener('flourish:sessionready', function (e) { /* e.detail */ });
window.addEventListener('flourish:action', function (e) { e.detail.payload; });
Events the loader has never heard of are relayed too, so a new one reaches your listeners without you changing anything. Agent actions arrive this way, as flourish:action (see Receiving Agent Actions).
Origin security. The chat only accepts messages from your page's origin. The hosted loader passes it for you. A hand-built iframe adds a c__flparentorigin parameter to the frame's URL, such as c__flparentorigin=https%3A%2F%2Fyour-app.example.com. If no origin can be determined, all inbound messages are rejected.
Example: using the hosted loader's wrapper
<script src="https://cdn.toflourish.org/agent-platform/v1/chatbot-embed.js" defer
data-agent-id="49b3512c-…"
data-mode="inline"></script>
<script>
// FlourishChatbot is defined by the loader once the iframe mounts.
window.addEventListener('message', function (e) {
// (Optionally verify e.origin against https://cdn.toflourish.org here.)
var msg; try { msg = JSON.parse(e.data); } catch (_) { return; }
if (!msg || msg.action !== 'fl__sessionready') return;
// Session is live, so it is now safe to drive the agent.
FlourishChatbot.setHostBusy(true, 'Searching…');
var results = /* run your local search */;
FlourishChatbot.attachData('results.json', results, 'application/json');
FlourishChatbot.sendMessage(
'Attached results.json (' + results.length + ' hits). Answer the pending question from it, citing sources.',
'host'
);
FlourishChatbot.setHostBusy(false);
});
</script>
Example: hand-rolled iframe (raw postMessage)
<iframe id="fl-chatbot"
src="https://cdn.toflourish.org/agent-platform/v1/chat.html?c__flagentid=49b3512c-…&c__flparentorigin=https%3A%2F%2Fyour-app.example.com"
allow="clipboard-write"></iframe>
<script>
var iframe = document.getElementById('fl-chatbot');
var CHATBOT_ORIGIN = 'https://cdn.toflourish.org'; // the chat page's origin
function post(payload) {
iframe.contentWindow.postMessage(JSON.stringify(payload), CHATBOT_ORIGIN);
}
window.addEventListener('message', function (e) {
if (e.origin !== CHATBOT_ORIGIN) return;
var msg; try { msg = JSON.parse(e.data); } catch (_) { return; }
if (msg && msg.action === 'fl__sessionready') {
post({ action: 'fl__setHostBusy', busy: true, label: 'Searching…' });
post({ action: 'fl__attachData', name: 'results.json', data: myResults, contentType: 'application/json' });
post({ action: 'fl__sendMessage', text: 'Attached results.json. Answer from it.', origin: 'host' });
post({ action: 'fl__setHostBusy', busy: false });
}
});
</script>
Note on host-origin messages.
origin: 'host'messages are delivered to the agent tagged as host notifications rather than user turns, and are hidden from the visible transcript (live and on history reload). Whether a host-origin message elicits a response is decided by the platform. If a message doesn't produce a reply, that is the expected behavior for a notification.
Minting the session yourself
Most embeds do not need this. The chat window opens its own connection when it loads, and that is the right answer whenever the agent and the data belong to the same organization. Read this only if the agent has to run against data belonging to whoever is visiting, rather than to whoever owns the agent.
The credential that says whose data a conversation runs on cannot go in a web page. So the exchange happens on a server, and the browser carries only the short-lived token that comes back.
Set data-session="host" on the script tag. That tells the chat window not to open a connection of its own, and it will wait to be handed one.
<script src="https://cdn.toflourish.org/agent-platform/v1/chatbot-embed.js" defer
data-agent-id="49b3512c-..."
data-session="host"></script>
Your server mints the session exactly as a web app's session endpoint does, and your page hands the result in:
FlourishChatbot.setSession({
conversationId: '79a1df5e-13ff-4b81-aff3-a4f04a8324bf',
token: 'eyJhbGciOi...',
wsUrl: 'wss://...',
agentName: 'Support',
chatbotTitle: 'Ask us anything'
});
A session missing conversationId or wsUrl is ignored. The rest are cosmetic. They fill in the agent's name and the panel title before the first message arrives, and the chat falls back to what it already knows without them.
Four things to know:
setSessiondoes not wait forfl__sessionready, unlike every other method on the wrapper. The session is what makes the window ready, so holding it back would leave the chat waiting on itself forever. Call it whenever your server answers.- Call it as early as you like. In bubble mode the frame is not built until the panel first opens, and a session minted before that is held and delivered when the frame appears. The chat window also asks for one as soon as it can listen, so neither side has to guess about the other.
- You have 20 seconds. If nothing arrives, the chat stops waiting and says so on the page: This page was expected to supply a chat session and did not. Check that it minted one and called setSession. That message is the one to look for when an embed sits on its loading state, and it means your endpoint, not the agent.
- Without
data-session="host"it does nothing, and the chat window opens its own connection as usual.
Receiving Agent Actions in the iframe
This is the reverse of fl__sendMessage. When the agent invokes a Custom Action, whether delivered as a structured action or a legacy :::action ... ::: text block, the chat window posts it to the parent as an fl__action message.
The message shape is { action: 'fl__action', payload: { type, ...params } }, where type is the action's action_type and the rest are its validated parameters, such as { type: 'query_corpus', query: 'venmo transfer', perPage: 15 }.
Receiving it (hosted loader): the loader relays it onto your page as a flourish:action event.
window.addEventListener('flourish:action', function (e) {
var action = e.detail.payload; // { type, ...params }
if (action.type === 'query_corpus') runSearch(action.query, action.perPage);
});
Receiving it (hand-rolled iframe):
window.addEventListener('message', function (e) {
if (e.origin !== CHATBOT_ORIGIN) return; // https://cdn.toflourish.org
var msg; try { msg = JSON.parse(e.data); } catch (_) { return; }
if ((msg.type || msg.action) === 'fl__action') {
handleAction(msg.payload); // payload = { type, ...params }
}
});
fl__action is posted only to the validated parent origin (the one derived from document.referrer or the c__flparentorigin param), never *. Custom actions with no return value (navigate, filter, bookmark) are one-way UI drivers. To feed results back to the agent, run the work in the host and return via fl__attachData plus fl__sendMessage.
Selecting the Agent at Runtime
To serve a different agent per page, read the ID at runtime and inject the loader tag yourself instead of pasting a static snippet.
<script>
var agentId = new URL(location.href).searchParams.get('agent') || 'DEFAULT_AGENT_ID';
var s = document.createElement('script');
s.src = 'https://cdn.toflourish.org/agent-platform/v1/chatbot-embed.js';
s.defer = true;
s.dataset.agentId = agentId; // the loader turns this into c__flagentid
document.body.appendChild(s);
</script>