> ## Documentation Index
> Fetch the complete documentation index at: https://flow.engineer/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Web chat on your site

> Put 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](https://flow.engineer/docs/messaging/web.md) instead.

## Add it to your agent

**1. Add the channel**

In `src/agent.ts`, add `webChat()` to `channels` with the sites the box may be on:

```ts src/agent.ts
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](https://flow.engineer/docs/traces.md) (channel `web`) and [scenario tests](https://flow.engineer/docs/testing.md) work as on every channel.

**2. 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:

```html Your page
<script async src="https://<agent address>/v1/web-chat/widget.js"
  data-title="Ask us"></script>
```

On [Flow's hosting](https://flow.engineer/docs/hosting.md) the agent address is `FLOW_AGENT_URL`, which
`npx @flow-engineer/flow deploy status` prints.

**3. Allow the agent in your site's CSP**

If your site sends a Content Security Policy, add these to the directives it already has:

```text Content-Security-Policy additions
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 use `data-identity-url`.
* `img-src`: the agent's photos, carousel pictures and `data-avatar`. Name their hosts instead
  of `https:` if you know them.
* `style-src` needs nothing. Safari before 16.4 falls back to a `<style>` element, which needs
  `'unsafe-inline'`. `font-src` needs 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

* **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](https://flow.engineer/docs/delivery-guarantees.md#the-web-chat)).
* **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

| 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](#page-context))                        |
| `data-identity-url` | Your same-origin URL that returns a signed-in visitor's token ([signed-in visitors](#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:

```css 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

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

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.

**1. 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](#on-flows-hosting)). For example:

```sh Terminal
openssl rand -base64 32
```

While you rotate it, give the agent both keys, comma-separated, or pass
`webChat({ identityKey: [newKey, oldKey] })`.

**2. Return a token from a signed-in route**

`sub` is the person's account key: the one your [`resolve`](https://flow.engineer/docs/conversations#one-person-across-messaging-channels)
returns for them on other channels. `phone` is optional: the number your backend verified is
theirs, in E.164. In Node:

```ts Your backend (Express)
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:

```json Identity token 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.

**3. Name the route on the script tag**

```html Your page
<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` (or `identityKey`) every visitor is anonymous.

### 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](https://flow.engineer/docs/conversations.md#one-person-several-conversations) has the rules.

## 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.

```html Product page
<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:

```ts A tool that reads it
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 `origins` set, a message whose `url` is 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

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

`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

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

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`

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`:

```ts src/agent.ts — the channel
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

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:

**1. Make a test page**

In a folder outside your agent, say `site/index.html`:

```html 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.

**2. Allow the test page and start the agent**

```sh Terminal
FLOW_WEB_CHAT_ORIGINS=http://localhost:5500 npm start
```

Then, in a second terminal, serve the page:

```sh Terminal
npx serve -l 5500 site
```

Setting the local site on the command line keeps it out of `.env`, which `deploy` sends as
secrets.

**3. 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

* **Set for you:** `FLOW_WEB_CHAT_SESSION_KEY` (one per environment, kept across deploys, so
  sessions survive them) and `FLOW_TRUST_PROXY=1`. `deploy` never 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 secret `FLOW_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 no
  `FLOW_WEB_CHAT_ORIGINS`. Fix it by listing your sites in `webChat({ origins })`, or set the secret
  and deploy again:

  ```sh Terminal
  printf '%s' 'https://www.example.com' | npx @flow-engineer/flow deploy secret FLOW_WEB_CHAT_ORIGINS --env production
  ```

  A secret takes effect on the next deploy.
* The box's routes are served at `FLOW_AGENT_URL` + `/v1/web-chat` ([routes](https://flow.engineer/docs/hosting.md#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

* Set `FLOW_WEB_CHAT_SESSION_KEY` (or `sessionKey`) to a fixed random key of at least 32 characters,
  the same in every process, so sessions survive restarts and deploys.
* Set `trustProxy` or `FLOW_TRUST_PROXY` to the number of proxies in front of the agent.
* Serve `agent.fetch(request)` for `/v1/web-chat` and everything under it. Allow streaming
  responses (server-sent events, up to 14 minutes) and do not buffer them.
* Use a durable database (`sqlite(file)` or `postgres(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](https://flow.engineer/docs/self-hosting.md#persist-conversations) 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

| `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           |

> **[Build your own box](https://flow.engineer/docs/messaging/web-chat-api.md)**
>
> The HTTP contract behind the chat box.

> **[Delivery guarantees](https://flow.engineer/docs/delivery-guarantees.md)**
>
> What a send means, ordering and duplicates on every channel.
