# Widget & API

> Embed the chat widget, set its attributes, and speak the envelope protocol from a custom client over the raw MoQT chat tracks.

You add live chat to a site with one web component. The component mints a
namespace-scoped token. It opens a persistent connection (QUIC first, WSS
fallback). It drives the conversation with the same flow/step engine that your
voice IVR uses: announcements, waits with queue position, forms, and
queue-to-skill routing to an AI text agent or a human. This page is the
reference for the two public surfaces:

- **The widget** — the `<clutchcall-chat>` web component and its config attributes.
- **The envelope protocol** — the JSON wire format on the chat tracks. Use it
  to build a custom client instead of the widget.

> **NOTE:**
> Chat is **widget/JS-first today**. There is no polyglot chat SDK and no typed
>   Python/Go/Rust client. The shipped surfaces are the embeddable web component
>   and the agent-side console. If you need chat outside the browser widget,
>   build a custom client against the envelope protocol documented below. That
>   path is raw, and only the `v` field versions it. Treat it as the low-level
>   contract, not as a supported SDK.

## Embed the widget

  1. **Load the component**
Include the widget script once, anywhere on the page. The script registers
    the custom element. It does nothing until you place the tag.

    ```html
    <script type="module" src="https://relay.firstbyte.fast/chat/widget.js"></script>
    ```
  2. **Place the tag**
Put the element where you want the launcher to appear. Only `org` is
    strictly required. The widget fetches a per-visitor token at runtime.

    ```html
    <clutchcall-chat
      org="org_abc"
      token-url="/api/chat-token"
      theme="auto">
    </clutchcall-chat>
    ```
  3. **Mint tokens from your backend**
The widget does not embed a secret. It calls `token-url` on your origin.
    Your endpoint returns a short-lived, namespace-scoped chat token (scoped to
    `chat/<org>/<conv>`). The control-plane API mints the token. Keep your API
    key server-side. Return only the token to the browser.

    ```ts
    // your backend — POST /api/chat-token
    const res = await fetch("https://portal.firstbyte.fast/v1/chat/tokens", {
      method: "POST",
      headers: { authorization: `Bearer ${process.env.CLUTCHCALL_CREDENTIALS}` },
      body: JSON.stringify({ org: "org_abc" }),
    });
    const { token } = await res.json();
    return Response.json({ token }); // browser passes this to the widget
    ```

### Config attributes

Set attributes on the tag (kebab-case) or through the DOM property of the same
name. Booleans follow HTML rules: a present attribute means `true`.

- **`org`** (`string`, required) — The workspace or org id that owns the conversation. It scopes the chat   namespace (`chat/<org>/…`) and the minted token.

- **`token-url`** (`string`) — An endpoint on **your** origin that returns `{ token }`. The widget calls it   once per visitor to mint a namespace-scoped token. Prefer this over a static   `token`. Then credentials never ship to the browser.

- **`token`** (`string`) — A pre-minted chat token, for when you fetch the token yourself and set it   directly. Do not use it together with `token-url`. The token is short-lived.   Set it before the session opens.

- **`relay`** (`string`, default `relay.firstbyte.fast`) — The relay host that the widget connects to. Override it only for a   self-hosted or region-pinned relay.

- **`theme`** (`'auto' | 'light' | 'dark'`, default `auto`) — Color scheme. `auto` follows the visitor's `prefers-color-scheme`.

- **`conversation`** (`string`) — Resume a specific conversation id instead of starting a new one. Omit to open   a fresh conversation and let the engine assign the id.

- **`greeting`** (`string`) — An optional opening line that the widget shows before the flow runs. It is   display-only. The flow engine still drives the actual conversation.

- **`launcher`** (`'bubble' | 'inline'`, default `bubble`) — `bubble` floats a launcher button. `inline` mounts the panel in the tag's own   box, so you can embed it in a page section.

> **NOTE:**
> The attribute surface is not final. `org` plus a token source (`token-url` or
>   `token`) is the firm contract. The display attributes above are convenience
>   wrappers, and their names can change. Pin the widget script version in
>   production. Check this page again after upgrades.

### Programmatic control

Get the element and drive it from JavaScript. You can open and close the panel,
attach prefilled context, and use lifecycle hooks.

```ts
const chat = document.querySelector("clutchcall-chat")!;

chat.open();                       // open the panel
chat.close();
chat.setContext({ plan: "pro", accountId: "acct_42" }); // attach visitor metadata
chat.addEventListener("conversation", (e) => {
  console.log("conversation id:", e.detail.conversation);
});
chat.addEventListener("message", (e) => {
  console.log(e.detail.role, e.detail.data.text); // mirror of server envelopes
});
```

The DOM events are a thin mirror of the envelope protocol. Each event carries
the underlying envelope in `event.detail`. If you only need to observe or
extend the widget, use these events rather than a custom client.

## The envelope protocol

Every message on a chat conversation, in both directions, is a single JSON
**envelope**. Each transport object carries one envelope. This is the contract
that you implement when you build a custom client.

```json
{
  "v": 1,
  "seq": 128,
  "ts": 1752345600123,
  "kind": "message",
  "role": "visitor",
  "data": { "mid": "m_71be04", "text": "Hi, I need help with my order" }
}
```

- **`v`** (`number`) — The protocol version. Currently `1`. Reject envelopes with a version that you   do not understand.
- **`seq`** (`number`) — A monotonic sequence number **per track**. Use it to order and de-duplicate   envelopes. A gap means that you missed an object.
- **`ts`** (`number`) — The author timestamp, in Unix epoch milliseconds.
- **`kind`** (`'message' | 'typing' | 'form' | 'form_result' | 'reaction' | 'event'`) — The envelope type. It determines the shape of `data` (below).
- **`role`** (`'visitor' | 'agent' | 'system'`) — The author. `visitor` is the end user. `agent` is a human or an AI text   agent. `system` is the flow engine (announcements, queue updates, forms).
- **`data`** (`object`) — The payload for the given kind. See the table below.

### `data` by kind

- **`message`** (`{ mid: string; text: string; attachments?: Attachment[]; reply_to?: { mid, preview } }`) — A chat message. `mid` is the author-owned message id (see [Message identity   and replies](#message-identity-and-replies)). `text` is the body. Optional   `attachments` carry [file references](#file-attachments). Optional `reply_to`   quotes an earlier message. The transcript shows the message.
- **`typing`** (`{ state: 'start' | 'stop' }`) — The typing indicator. Send `start`/`stop` as the visitor types. Show the   peer's indicator when you receive it.
- **`form`** (`{ formId: string; title?: string; fields: Field[] }`) — The engine **presents a form**. It blocks the flow until it receives a   matching `form_result`. Each `Field` has `{ name, label, type, required, options? }`.   Render the form and collect input.
- **`form_result`** (`{ formId: string; values: Record<string,unknown> }`) — Your reply to a `form`. Send it (role `visitor`) with the collected `values`.   The engine validates the values against the form definition and unblocks the   flow. On a validation failure, it presents the form again.
- **`reaction`** (`{ op: 'add' | 'remove'; emoji: string; target_mid: string }`) — Add or remove an emoji reaction on the message that `target_mid` identifies.   The engine relays the reaction to the other side and emits a `chat.reaction`   event. See [Reactions](#reactions).
- **`event`** (`{ name: string; [k: string]: unknown }`) — A lifecycle signal from the flow engine. Common `name` values: `queued`   (often with a `position`), `agent_joined`, `agent_left`, `resumed` (the AI   resumes after a human idles out), and `closed`. Treat unknown events as   advisory.

## Message identity and replies

Every `message` envelope carries a `mid`. This is an **author-owned message id,
unique in the conversation**. The engine stamps it and relays it. One message
points at another through the `mid`: both [reactions](#reactions) and quoted
replies address a target message by its `mid`.

- **`mid`** (`string`) — A stable identifier for a message, unique in the conversation. Every   `message` envelope has one. Use it as the React/render key and as the target   for reactions and quoted replies.
- **`reply_to`** (`{ mid: string; preview: string }`) — Present when the message is a **quoted reply**. `mid` identifies the quoted   message. `preview` is a short snippet of that message, used to show the quote   inline. Omit this field for a normal (non-reply) message.

```json
{
  "v": 1, "seq": 141, "ts": 1752345600890,
  "kind": "message", "role": "agent",
  "data": {
    "mid": "m_9f3a2c",
    "text": "Yes — I can see order #4471 shipped this morning.",
    "reply_to": { "mid": "m_71be04", "preview": "where's my order?" }
  }
}
```

> **NOTE:**
> On the widget, quoted replies work with no code. The `mid`/`reply_to` fields
>   matter when you [build a custom client](#build-a-custom-client) or when you
>   must understand the wire.

## File attachments

A message can carry files. The **bytes travel out-of-band over HTTP, not over
the chat tracks**. First upload the file to the control-plane API. Then publish
a normal `message` envelope that references the returned attachment meta. This
keeps large transfers off the realtime tracks.

> **NOTE:**
> The widget has this fully built in: a paperclip attach button, drag-and-drop,
>   and a multi-file picker. The flow below is for custom clients and for readers
>   who study the wire.

  1. **Upload the bytes**
`POST` the raw file bytes to the upload endpoint with the session token.
    Put the filename in the `x-cc-filename` header. Set `Content-Type` to the
    file's MIME type. The response is the attachment meta.

    
    **TypeScript:**
```ts
    const res = await fetch("https://engine.firstbyte.fast/v1/chat/upload", {
      method: "POST",
      headers: {
        authorization: `Bearer ${token}`,      // the session token
        "x-cc-filename": file.name,
        "content-type": file.type,             // e.g. image/png
      },
      body: file,                              // raw file bytes
    });
    const attachment = await res.json();
    // { id, name, size, mime, url }
    ```

    **Python:**
```python
    with open(path, "rb") as f:
        res = httpx.post(
            "https://engine.firstbyte.fast/v1/chat/upload",
            headers={
                "authorization": f"Bearer {token}",
                "x-cc-filename": name,
                "content-type": mime,
            },
            content=f.read(),                  # raw file bytes
        )
    attachment = res.json()  # { id, name, size, mime, url }
    ```
    

    > **WARNING:**
> The endpoint enforces allowed MIME types and a maximum size. It rejects
>       uploads outside the allow-list and uploads over the size limit. Validate
>       the file client-side and show the error to the user.
  2. **Publish a message that references it**
Send a normal `message` envelope. Set `data.attachments` to an array of the
    metas that you received. An attachment-only message is permitted. `text`
    can be empty.

    ```ts
    await client.send(JSON.stringify({
      v: 1, seq: seq++, ts: Date.now(),
      kind: "message", role: "visitor",
      data: { mid: newMid(), text: "", attachments: [attachment] },
    }));
    ```
  3. **Download on the other side**
Each attachment `url` resolves to the download endpoint. The other side
    fetches it with the session token to get the bytes.

    ```
    GET https://engine.firstbyte.fast/v1/chat/attachment/<conv>/<id>/<name>
    ```

- **`Attachment`** (`object`) — The meta that the upload returns and that `data.attachments` carries.        - **`id`** (`string`) — The attachment id, unique in the conversation.     - **`name`** (`string`) — The original filename.     - **`size`** (`number`) — The size in bytes.     - **`mime`** (`string`) — The MIME type (from the allow-list).     - **`url`** (`string`) — The download URL for the bytes. Object storage serves them.   

When an upload lands, the engine emits a `chat.file.uploaded` event
`{ role, count }`. The transcript records a `[file: names]` line. Thus an AI
text agent knows that a file arrived, although it never sees the raw bytes.

## Reactions

A reaction is its own envelope `kind`. It adds or removes an emoji on a
message. The message's [`mid`](#message-identity-and-replies) identifies it.
The engine relays the reaction to the other side and emits a `chat.reaction`
event.

- **`data`** (`{ op: 'add' | 'remove'; emoji: string; target_mid: string }`) —      - **`op`** (`'add' | 'remove'`) — `add` adds the reaction. `remove` removes it.     - **`emoji`** (`string`) — The emoji.     - **`target_mid`** (`string`) — The `mid` of the target message.   

```ts
// react to a message
await client.send(JSON.stringify({
  v: 1, seq: seq++, ts: Date.now(),
  kind: "reaction", role: "visitor",
  data: { op: "add", emoji: "👍", target_mid: "m_9f3a2c" },
}));
```

The engine emits a `chat.reaction` event `{ role }` when it relays a reaction.

> **NOTE:**
> The widget has reactions built in: an emoji picker, a hover quick-reaction
>   strip, and reaction pills with counts. They work with no code. The envelope
>   above is for custom clients.

## Build a custom client

If you need chat where the web component cannot run, connect directly to the
chat tracks over the persistent connection. Exchange envelopes yourself.

### Track layout

A conversation lives under the namespace `chat/<org>/<conv>` with three tracks:

| Track    | Direction              | You (custom client)                         |
| -------- | ---------------------- | ------------------------------------------- |
| `client` | visitor → engine       | **Publish** your envelopes here             |
| `server` | engine → visitor       | **Subscribe** — this is your only read      |
| `agent`  | human agent → engine   | Do not touch — the engine relays it to `server` |

The engine folds the human `agent` track into `server`. A client thus keeps a
**single subscription** (`server`) and a single publish (`client`). This holds
whether an AI or a human is on the other end.

  1. **Open the session**
Connect to the relay with your namespace-scoped token. The transport is
    QUIC first, with a WSS fallback. This is the same connection that the
    widget uses.
  2. **Publish to `client`, subscribe to `server`**
Announce and publish `chat/<org>/<conv>/client`. Subscribe to
    `chat/<org>/<conv>/server`. Each transport object that you send or receive
    is exactly one JSON envelope.
  3. **Exchange envelopes**
Send `message` and `typing` envelopes (role `visitor`) on `client`. Render
    everything from `server`. When a `form` arrives, collect input. Reply with
    a `form_result`. Use `event` envelopes to follow queue position and handoff
    state.

```ts
// Pseudocode over your MoQT/WebTransport client — one JSON object per envelope.
const base = `chat/${org}/${conv}`;
const session = await connectChat({ relay: "relay.firstbyte.fast", token });

const client = await session.publish(`${base}/client`);
const server = await session.subscribe(`${base}/server`);

// send a message (stamp your own author-owned mid)
let seq = 0;
await client.send(JSON.stringify({
  v: 1, seq: seq++, ts: Date.now(),
  kind: "message", role: "visitor",
  data: { mid: newMid(), text: "Hi, I need help with my order" },
}));

// receive + handle
for await (const obj of server) {
  const env = JSON.parse(obj);
  switch (env.kind) {
    case "message":  render(env.role, env.data); break; // mid, text, attachments, reply_to
    case "reaction": applyReaction(env.data); break;    // { op, emoji, target_mid }
    case "typing":   setPeerTyping(env.data.state === "start"); break;
    case "event":    handleEvent(env.data); break; // queued/position/agent_joined/closed
    case "form": {
      const values = await collect(env.data.fields);
      await client.send(JSON.stringify({
        v: 1, seq: seq++, ts: Date.now(),
        kind: "form_result", role: "visitor",
        data: { formId: env.data.formId, values },
      }));
      break;
    }
  }
}
```

> **WARNING:**
> This is the low-level contract, not a supported client library. You own
>   transport setup, reconnection, and `seq` gap handling. The `v` field is the
>   only compatibility guarantee. Build defensively. Pin behavior against a known
>   widget version.

## Related

  - **[Chat overview](/modalities/chat/details)** — How the flow engine, AI text agents, human handoff, forms, and skill queues
    fit together.
  - **[Chat cookbook](/modalities/chat/cookbook)** — Copy-pasteable recipes: embed on a page, present a form, route to a skill,
    hand off to a human.
  - **[Chat recipes](/modalities/chat/recipes)** — End-to-end builds: a support widget with AI-to-human handoff and lead
    capture.
  - **[CSTA CTI](/modalities/voice/cti/overview)** — Drive third-party call control (makeCall, holdCall, transferCall) over the
    same engine connection from an ISV/SDK client.
