> ## 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 HTTP contract

> Build your own chat box, or a mobile app's chat, against the routes webChat() serves: sessions, history, events over SSE or long-polling, and messages.

The [web chat](https://flow.engineer/docs/messaging/web-chat.md)'s script is one client of this contract. Use it to build a box
of your own. Narrow `webChat({ info })` to what your box can show, so the agent sends only that.

Every route is under the channel's `base`, `/v1/web-chat` by default. Bodies are JSON. Every call
but `POST /session` and the script files needs `Authorization: Bearer <session>`. Browsers on the
[allowed sites](https://flow.engineer/docs/messaging/web-chat.md#allowed-sites) get CORS headers; any other `Origin` gets
`403 origin_not_allowed`.

## Routes

| Call                                                    | What it does                                                                                                                    |
| ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `POST /session { visitor?, identity? }`                 | `200 { session, expires_at, signed_in, visitor?, limits }`: a session for 1 hour. `limits.message_chars` is the longest message |
| `GET /history`                                          | `200 { messages, cursor }`: the conversation on this channel, oldest first, and the newest event number                         |
| `GET /events?after=<seq>`                               | The events after `seq`: server-sent events with `Accept: text/event-stream`, else a long-poll                                   |
| `POST /messages { id, text, choice?, page?, context? }` | `202 { id }` once the agent's store has the message                                                                             |
| `GET /widget.js`, `GET /widget/<name>.js`               | The chat box's script, no session needed                                                                                        |

### Sessions

* **A new anonymous visitor** sends `{}`. The answer's `visitor` is their credential: store it.
* **A returning visitor** sends the stored `visitor`. The answer has it renewed, valid 30 days
  from now: store the new one.
* **A signed-in visitor** sends `identity`, a token from your backend
  ([signed-in visitors](https://flow.engineer/docs/messaging/web-chat.md#signed-in-visitors)). The answer has `signed_in: true`
  and no `visitor`. On `401 identity_invalid`, start an anonymous session instead.
* `expires_at` is in milliseconds since the epoch. Start a new session before it, and on any
  `401 session_invalid`.

### History and the cursor

`GET /history` returns up to the last 200 entries. Entries older than the outbox, which keeps a
week, have `seq: 0`. Draw the history, then read events after its `cursor`.

### Events

With `Accept: text/event-stream`, the answer is a stream of:

| SSE event   | Data                                                                                   |
| ----------- | -------------------------------------------------------------------------------------- |
| `hello`     | `{ cursor }`, first, once the stream is open                                           |
| `message`   | One event (below); its `seq` is also the SSE `id`                                      |
| `live`      | `{ typing, status }`: whether the agent is typing, and its status line or `null`       |
| `reset`     | `{}`: the cursor cannot be continued (older events deleted). Read `GET /history` again |
| `reconnect` | `{ cursor }`, after 14 minutes: open a new stream after the last event you showed      |

A comment line (`: ping`) comes every 15 seconds.

Without that header, the call is a long-poll that waits up to 25 seconds:
`200 { events, cursor, live }`, or `200 { reset: true }`.

Remember the last `seq` you showed. **Show each `seq` once**, and ignore one you already showed:
after a reconnect, a restart or a deploy, ask for `after=<last seq shown>` and nothing is lost or
repeated. One visitor may hold 6 event requests open at once (their tabs).

### Messages

```json POST /v1/web-chat/messages
{
  "id": "m-7f3a2c",
  "text": "Is this one still available?",
  "page": { "url": "https://www.example.com/listing/ps5", "title": "PS5 Disc" },
  "context": { "listing": "ps5-disc" }
}
```

* `id` is your own id for the message: 1 to 64 letters, digits, `_`, `.`, `:` or `-`. Send the same
  `id` on every retry; the agent takes it once.
* A tap on a button or a list row is `text: <label>, choice: { id, title: <label> }`.
* `page` and `context` together are at most 1 KB, and `page.url` must be on an allowed site.
* `202 { id }` means the agent's store has the message.
* The visitor's own message comes back as a `message` event (`from: "person"`), so every tab shows
  it.

## Events

An event is `{ seq, at, type, ... }`, `at` in milliseconds since the epoch:

| `type`                      | Fields                                                                                                                                                                                     |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `message`, `from: "agent"`  | `id`, Markdown `text`, and optionally `replyTo`, `buttons` (`[{ id, label }]`), `list` (`{ button, rows: [{ id, title, description? }] }`), `link` (`{ label, url }`), `image` (`{ url }`) |
| `message`, `from: "person"` | `id`, `text`, `choice?`                                                                                                                                                                    |
| `carousel`                  | `id`, `text`, `cards` (`[{ image: { url }, text?, buttons?, link? }]`)                                                                                                                     |
| `reaction`                  | `messageId`, `emoji`                                                                                                                                                                       |

Not on the web chat yet: files from visitors, edits and unsends.

## Errors

Errors are `{ error, message, retry_after? }`; a `429` also sends `Retry-After` in seconds.

| Status | `error`                   | What to do                                                                                     |
| ------ | ------------------------- | ---------------------------------------------------------------------------------------------- |
| 400    | `bad_request`             | Fix the request; `message` says what is wrong                                                  |
| 401    | `session_invalid`         | Start a new session                                                                            |
| 401    | `identity_invalid`        | Start an anonymous session                                                                     |
| 403    | `origin_not_allowed`      | Add the site to `origins`                                                                      |
| 413    | `too_large`               | The body is over 16 KB                                                                         |
| 429    | `rate_limited`            | Wait `retry_after` seconds ([limits](https://flow.engineer/docs/messaging/web-chat.md#limits)) |
| 503    | `not_ready`, `not_stored` | Send the same request again after `retry_after`                                                |
| 500    | `failed`                  | Try again                                                                                      |

> **A minimal client in the browser**
>
> This sketch uses long-polling only. `AGENT` is the agent's address, and `show(event)` is your own
> function that draws an event:
>
> ```ts Minimal web chat client
> const AGENT = "https://agent.example.com/v1/web-chat";
> const stored = localStorage.getItem("chat-visitor") ?? undefined;
> const s = await (await fetch(`${AGENT}/session`, {
>   method: "POST",
>   headers: { "content-type": "application/json" },
>   body: JSON.stringify({ visitor: stored }),
> })).json();
> if (s.visitor) localStorage.setItem("chat-visitor", s.visitor);
> const auth = { authorization: `Bearer ${s.session}` };
>
> const { messages, cursor } = await (await fetch(`${AGENT}/history`, { headers: auth })).json();
> messages.forEach(show);
> let last = cursor;
>
> async function send(text: string) {
>   const id = crypto.randomUUID();
>   await fetch(`${AGENT}/messages`, {
>     method: "POST",
>     headers: { ...auth, "content-type": "application/json" },
>     body: JSON.stringify({ id, text, page: { url: location.href } }),
>   });
> }
>
> for (;;) {
>   const got = await (await fetch(`${AGENT}/events?after=${last}`, { headers: auth })).json();
>   if (got.reset) break; // read the history again
>   for (const event of got.events) if (event.seq > last) (last = event.seq), show(event);
> }
> ```
>
> A production client also renews the session before `expires_at`, retries failed sends with the
> same `id`, honours `retry_after` and prefers server-sent events.
