Web chat on your site
Web chatPut your agent in a chat box on your own website with one script tag. The agent serves the box itself: no Flow Messaging, no other service.
Visitors to your site ask the agent what they would ask a person: “is this one still available?”,
“when can it arrive?”. They get the same tools, skills and conversation as on WhatsApp, in a box on
your page. Add webChat() to the agent; it serves the box and its API under /v1/web-chat.
Requires Flow Framework 0.12.0 or later. To keep your own chat UI and only use Flow’s conversation runtime, see your own chat UI instead.
Add it to your agent
Section titled “Add it to your agent”Add the channel
In src/agent.ts, add webChat() to channels with the sites the box may be on:
import { webChat } from "@flow-engineer/flow";
// In buildAgent:const agent = createAgent({ // ...the starter's options, host: options.host included channels: [ ...unconnected, ...connected, webChat({ origins: ["https://www.example.com"] }), terminal.channel, ],});webChat() is named web, and two channels cannot share a name. The starter has a placeholder
customUI("web", provider) in its unconnected list: delete it from that list, or the agent
stops with two channels have the same name. Keep webChat() outside unconnected, which is
empty on Flow’s hosting.
The agent’s send tool on this channel is send_web. beforeSend, content rules,
traces (channel web) and scenario tests work as on every channel.
Add the script tag to your site
Put one tag on every page that should show the box. The agent serves the script, so it always matches the agent’s version:
<script async src="https://<agent address>/v1/web-chat/widget.js" data-title="Ask us"></script>On Flow’s hosting the agent address is FLOW_AGENT_URL, which
npx @flow-engineer/flow deploy status prints.
Allow the agent in your site's CSP
If your site sends a Content Security Policy, add these to the directives it already has:
script-src https://<agent address>;connect-src https://<agent address> 'self';img-src https:;script-src: the launcher script and the panel it loads.connect-src: the box’s calls to the agent;'self'only if you usedata-identity-url.img-src: the agent’s photos, carousel pictures anddata-avatar. Name their hosts instead ofhttps:if you know them.style-srcneeds nothing. Safari before 16.4 falls back to a<style>element, which needs'unsafe-inline'.font-srcneeds nothing unless you set the box’s font to your own web font.
Sites that enforce Trusted Types (require-trusted-types-for 'script') are not supported yet.
Check it worked: an “Ask us” button appears at the bottom right of the page. Open it and ask “What’s on the menu?”: the agent’s reply appears in the box. Reload the page and open it again: the conversation is still there.
What the visitor sees
Section titled “What the visitor sees”- A launcher button, then a chat panel on first open (a full-screen sheet on phones). The launcher script is small; the panel loads only when opened.
- The agent’s messages with Markdown, reply buttons, a list to pick from, a link button, photos and carousels. A tap on a button or a list row is sent as the visitor’s answer.
- Typing and status lines while the agent works.
- The same conversation in every tab and after a reload. The box shows each message once, in order, through dropped connections, restarts and deploys (delivery guarantees).
- English or Hindi for the box’s own words (
data-lang, else the page’s language).
Not on the web chat yet: files and photos from visitors, and editing or unsending a message.
Script tag options
Section titled “Script tag options”| Attribute | What it does |
|---|---|
data-title | The launcher’s and panel’s title. Default “Chat with us” |
data-greeting | A first message shown before the visitor writes |
data-avatar | An https image shown in the panel’s header |
data-context | JSON of the page’s details, sent with each message (page context) |
data-identity-url | Your same-origin URL that returns a signed-in visitor’s token (signed-in visitors) |
data-lang | en or hi. Default: the page’s lang, else the browser’s |
data-theme | light, dark or auto (default, follows the system) |
data-position | right (default) or left |
The page can also drive the box through window.FlowChat:
| Call | What it does |
|---|---|
FlowChat.open(), FlowChat.close() | Opens or closes the panel |
FlowChat.setContext({ ... }) | Replaces the page details sent with each message from now on |
FlowChat.signIn() | The visitor signed in on your site: the box starts a signed-in session |
FlowChat.signOut() | The visitor signed out: the box goes back to an anonymous visitor |
To match your brand, set these on the box’s element, flow-chat, in your stylesheet:
flow-chat { --flow-chat-accent: #2457d6; /* the launcher, the visitor's messages, the send button */ --flow-chat-accent-text: #ffffff; /* text on the accent */ --flow-chat-font: Inter, system-ui, sans-serif; --flow-chat-radius: 16px;}Who the visitor is
Section titled “Who the visitor is”By default every visitor is anonymous. The box gets an id that lasts 30 days, renewed while
they come back, and keeps it in the browser. The agent knows them as web:visitor:<id>
(context.person in tools). Safari deletes a script’s storage after 7 days without a visit; the
visitor then starts a new conversation.
Signed-in visitors
Section titled “Signed-in visitors”When the visitor is signed in on your site, make them the same person the agent knows on other channels, with their whole conversation and every tool. Your backend signs a short identity token; the box exchanges it for a session.
Share a key between your backend and the agent
Make a random key of at least 32 characters and set it as FLOW_WEB_CHAT_IDENTITY_KEY in both
places (on Flow’s hosting, a secret; see hosting). For example:
openssl rand -base64 32While you rotate it, give the agent both keys, comma-separated, or pass
webChat({ identityKey: [newKey, oldKey] }).
Return a token from a signed-in route
sub is the person’s account key: the one your resolve
returns for them on other channels. phone is optional: the number your backend verified is
theirs, in E.164. In Node:
import { webChatIdentity } from "@flow-engineer/flow/web-chat";
app.get("/api/flow-chat-identity", requireLogin, (req, res) => res.json({ token: webChatIdentity({ sub: req.user.id, phone: req.user.verifiedPhone, // "+919876543210", or undefined key: process.env.FLOW_WEB_CHAT_IDENTITY_KEY!, }), }),);In any other language, sign an HS256 JWT with the same key and these claims:
{ "sub": "member-7", "phone": "+919876543210", "aud": "flow-web-chat", "iat": 1793174400, "exp": 1793174700 }sub is 1 to 200 characters. phone, when present, is E.164 (+ and 7 to 15 digits); any
other value makes the token invalid. exp is at most 10 minutes after the token is made
(webChatIdentity uses 5 by default). The token only starts a session; it is not stored.
Name the route on the script tag
<script async src="https://<agent address>/v1/web-chat/widget.js" data-identity-url="/api/flow-chat-identity"></script>The box calls the URL with the visitor’s cookies, so it must be on the page’s own origin. Call
FlowChat.signIn() after your site signs someone in without a page load, and
FlowChat.signOut() when they sign out.
Check it worked: sign in on your site and ask the agent something only that account’s tools can
answer. In a tool, context.key is the sub you signed and context.person is key:<sub>.
- A visitor without a valid token, or with no
data-identity-url, chats anonymously. An invalid token is never an error in the box: it carries on as an anonymous visitor. - An anonymous visitor’s chat stays apart from their account when they sign in.
- Without
FLOW_WEB_CHAT_IDENTITY_KEY(oridentityKey) every visitor is anonymous.
Their WhatsApp chat, in the web chat
Section titled “Their WhatsApp chat, in the web chat”With phone in the token, the web chat and the person’s WhatsApp, SMS or iMessage chat from that
number stay two conversations, but each turn of one is shown their recent messages in the other.
Replies still go out only where the person wrote.
For example, a person asked on WhatsApp (+919876543210) “Is my bid on the PS5 still the
highest?” and the agent answered “Yes, your ₹30,000 bid leads.” They then sign in on your site
with a token carrying that number and ask in the box “what are my bids?”.
Check it worked: the box answers with the PS5 bid, and nothing new goes to WhatsApp. When they
write on WhatsApp next, that turn is shown the web chat too. Put only a number your backend verified
in phone, never one typed in the chat: an anonymous visitor who types a number stays apart.
One person, several conversations has the rules.
Page context
Section titled “Page context”Each message carries the page the visitor wrote from (url, title) and your site’s own details:
data-context on the script tag, or FlowChat.setContext() for pages that change without a load.
<script async src="https://<agent address>/v1/web-chat/widget.js" data-context='{"listing":"ps5-disc"}'></script>In this tool, inventory stands for your own backend client:
const isAvailable = tool({ name: "is_available", description: "Whether the listing the person is looking at is still available", input: z.object({}), run: async (_input, context) => { const listing = context.page?.context?.listing; if (typeof listing !== "string") return "Ask which listing they mean."; return inventory.status(listing); },});- The agent reads the page as one line of data in its turn, never as instructions. Tools get it as
context.page:{ url, title?, context? }, from the visitor’s latest message. - The page and the details together are at most 1 KB. To fit, the box drops the page’s title, then your details, then the URL’s query string.
- With
originsset, a message whoseurlis not on one of your sites is refused. - It comes from the visitor’s browser: treat it as a hint, never as proof of who they are or what
they may see. Check access in your tool against
context.key.
Protection
Section titled “Protection”The box is public: anyone can open your page, and a script can call the agent directly. Each layer stops something different.
| Layer | What it stops | What it does not stop |
|---|---|---|
Allowed sites (origins) | Other websites putting your agent on their pages | A script or server calling the API directly: it can send any Origin |
| Sessions | Calls without a session: every call but starting one needs it | A script that starts sessions like a browser |
| Limits | Scripts and floods running up your model spend | Abuse within the limits |
Allowed sites
Section titled “Allowed sites”origins lists where the box may be: https://www.example.com, or https://*.example.com for
any subdomain (not the bare domain, which you list separately). http:// is allowed only for
localhost and 127.0.0.1. FLOW_WEB_CHAT_ORIGINS, comma-separated, adds to the list.
With no sites listed at all, any website may embed the agent and spend its credit. Use that only
for development: Flow’s hosting refuses it in production (web_chat_open).
Sessions
Section titled “Sessions”The box starts a session that lasts 1 hour and renews it before it ends. Sessions and anonymous
ids are signed with sessionKey (default FLOW_WEB_CHAT_SESSION_KEY, at least 32 characters).
Flow’s hosting sets it for you. Without one, the agent makes a random key per process, and every
session and anonymous visitor ends when the process restarts.
Limits
Section titled “Limits”Set any of these in webChat({ limits }):
WebChatLimits | Default |
|---|---|
newVisitorsPerMinute, newVisitorsPerDay | 20 and 300 new visitors per network address. Mobile networks put many people behind one address |
messagesPerMinute, messagesPerDay | 8 and 60 per anonymous visitor |
signedInMessagesPerMinute, signedInMessagesPerDay | 15 and 300 per signed-in visitor |
messagesPerAddressPerMinute | 60 per network address, all its visitors together |
messageChars | 2,000 characters a message (request bodies at most 16 KB) |
turnsAtOnce | 20 web chat turns at once; more wait their turn, with typing shown |
- Wrong identity tokens are refused after 10 from one address in 15 minutes.
- A visitor over a limit sees “Too many messages. Try again in a while.” in the box.
- Limits are held in the process’s memory, so with several processes each counts its own. Flow’s hosting runs one process per environment, so its limits are exact.
- There is no cap on the agent’s web chat messages a day. The workspace’s model credit limits still apply, and an agent-wide daily cap may come in a later release.
- Logs carry ids and counts, never message text, network addresses or tokens.
Behind a proxy: trustProxy
Section titled “Behind a proxy: trustProxy”The limits per network address need the visitor’s real address. Behind a load balancer or reverse
proxy, the agent sees the proxy’s address unless you say how many proxies append to
X-Forwarded-For:
webChat({ origins: ["https://www.example.com"], trustProxy: 1 });or FLOW_TRUST_PROXY=1. The client’s address is that many entries from the end of
X-Forwarded-For; entries before it are what the client sent and are ignored. Set it to exactly the
number of proxies you run: a higher number lets a client choose its own address. Without it, the
limits per network address are off (the agent logs a warning once); the limits per visitor still
hold. Flow’s hosting sets it for you.
Test it locally
Section titled “Test it locally”The playground plays webChat() as its web simulator, so test people can use it like any other
channel. To try the real box on a page:
Make a test page
In a folder outside your agent, say site/index.html:
<!doctype html><html lang="en"> <title>Web chat test</title> <h1>Our café</h1> <script async src="http://localhost:8788/v1/web-chat/widget.js" data-title="Ask us" data-context='{"page":"home"}'></script></html>Use the port npm start prints if it is not 8788.
Allow the test page and start the agent
FLOW_WEB_CHAT_ORIGINS=http://localhost:5500 npm startThen, in a second terminal, serve the page:
npx serve -l 5500 siteSetting the local site on the command line keeps it out of .env, which deploy sends as
secrets.
Chat
Open http://localhost:5500, open the box and ask “What’s on the menu?”.
Check it worked: the reply appears in the box, and after a reload the conversation is still
there. The same page served on any other port gets no box: its calls are refused with
403 origin_not_allowed.
Locally, without FLOW_WEB_CHAT_SESSION_KEY, a restart of npm start ends the box’s session and
anonymous id: the box starts a new conversation.
On Flow’s hosting
Section titled “On Flow’s hosting”-
Set for you:
FLOW_WEB_CHAT_SESSION_KEY(one per environment, kept across deploys, so sessions survive them) andFLOW_TRUST_PROXY=1.deploynever sends either from your.env. -
You set: your sites, in code or as the secret
FLOW_WEB_CHAT_ORIGINS, and for signed-in visitors the secretFLOW_WEB_CHAT_IDENTITY_KEY. -
Production refuses a web chat open to any site (
web_chat_open); staging deploys it with a warning. Promoting such a staging version is refused too while production has noFLOW_WEB_CHAT_ORIGINS. Fix it by listing your sites inwebChat({ origins }), or set the secret and deploy again:Terminal printf '%s' 'https://www.example.com' | npx @flow-engineer/flow deploy secret FLOW_WEB_CHAT_ORIGINS --env productionA secret takes effect on the next deploy.
-
The box’s routes are served at
FLOW_AGENT_URL+/v1/web-chat(routes).
The agent’s store must be on Flow Framework 0.12 for an agent that uses 0.12: until Flow updates it,
the deploy is refused with chinwag_too_new.
Self-hosting
Section titled “Self-hosting”- Set
FLOW_WEB_CHAT_SESSION_KEY(orsessionKey) to a fixed random key of at least 32 characters, the same in every process, so sessions survive restarts and deploys. - Set
trustProxyorFLOW_TRUST_PROXYto the number of proxies in front of the agent. - Serve
agent.fetch(request)for/v1/web-chatand everything under it. Allow streaming responses (server-sent events, up to 14 minutes) and do not buffer them. - Use a durable database (
sqlite(file)orpostgres(pool)). The box’s messages wait in an outbox in the store; with the default in-memory store they are lost at a restart. - A store of your own that forwards Store calls must run
Store migration 12, serve its six new methods and pass
addIdentity’s verified number on (see the framework’s 0.12.0 release notes).
Options
Section titled “Options”webChat() option | What it does |
|---|---|
origins | The sites the box may be on. Joined with FLOW_WEB_CHAT_ORIGINS |
identityKey | The key, or keys while rotating, your backend signs identity tokens with. Default FLOW_WEB_CHAT_IDENTITY_KEY (comma-separated) |
sessionKey | Signs sessions and anonymous ids. Default FLOW_WEB_CHAT_SESSION_KEY |
trustProxy | How many proxies append to X-Forwarded-For. Default FLOW_TRUST_PROXY |
limits | WebChatLimits, above |
name, base | The channel’s name (default web, send tool send_<name>) and where its routes answer (default /v1/web-chat) |
info, instructions | What your box can show, over WEB_CHAT (narrow it for a box of your own); your guidance for the agent on this channel |
Thanks, we read every comment.