# Cookbook

> Copy-pasteable chat recipes: embed on a page, present a form, route to a skill, attach an AI agent, hand off to a human, theme, and mount programmatically.

Short, self-contained recipes for the embeddable chat widget. Each recipe does
a single job. You can paste it straight into a page or your backend. The widget
is the `<clutchcall-chat>` web component. It mints a conversation-scoped
token. It opens a real-time session (QUIC first, WebSocket fallback). It drives
the **same visual flow / step engine** as FirstByte Voice: announcements,
waits with queue position, forms, and skill queues. It also supports AI text
agents and human handoff.

> **NOTE:**
> Chat is **widget- and JS-first today.** There is no polyglot SDK for chat.
> Embed the web component and configure it, or speak the
> [envelope protocol](/modalities/chat/sdk-methods) directly from a custom client
> over raw MoQT. Server-side token minting uses your normal control-plane
> credentials.

## Add chat to a webpage

Add the widget to a page in two steps. First, mint a **conversation-scoped
token** on your server. Never ship your API key to the browser. Then render the
element with that token.

  1. **Mint a scoped token server-side**
Exchange your server credential for a short-lived token. The token is bound
    to one conversation namespace (`chat/<org>/<conv>`). Do this in a backend
    route. Give the result to the page.

    
    **server.ts (TypeScript):**
```ts
    // POST /api/chat-token  →  { token, conversation, expiresAt }
    export async function mintChatToken(orgId: string) {
      const res = await fetch("https://engine.firstbyte.fast/v1/chat/tokens", {
        method: "POST",
        headers: {
          "authorization": `Bearer ${process.env.CLUTCHCALL_CREDENTIALS}`,
          "content-type": "application/json",
        },
        body: JSON.stringify({
          org: orgId,
          flow: "support",       // the flow/step program to run
          ttlSec: 900,           // token lifetime
        }),
      });
      if (!res.ok) throw new Error(`mint failed: ${res.status}`);
      return res.json(); // { token, conversation, expiresAt }
    }
    ```

    **server.py (Python):**
```python
    import os, requests

    def mint_chat_token(org_id: str) -> dict:
        res = requests.post(
            "https://engine.firstbyte.fast/v1/chat/tokens",
            headers={"authorization": f"Bearer {os.environ['CLUTCHCALL_CREDENTIALS']}"},
            json={"org": org_id, "flow": "support", "ttlSec": 900},
            timeout=10,
        )
        res.raise_for_status()
        return res.json()  # { token, conversation, expiresAt }
    ```

    **curl:**
```bash
    curl -sX POST https://engine.firstbyte.fast/v1/chat/tokens \
      -H "authorization: Bearer $CLUTCHCALL_CREDENTIALS" \
      -H "content-type: application/json" \
      -d '{"org":"org_abc","flow":"support","ttlSec":900}'
    ```
    

    - **`org`** (`string`, required) — Your organization or workspace id. It scopes the conversation namespace.
    - **`flow`** (`string`) — The id of the flow that the conversation runs. You build the flow in the       console with the same step engine as voice. Omit this field to use your       default flow.
    - **`ttlSec`** (`number`, default `900`) — The token lifetime in seconds. When the token expires, the widget mints a       new one through your endpoint.

  2. **Render the widget with the token**
Load the widget loader once. Then place the element and pass the minted
    token. The code below is the complete, self-contained embed.

    ```html index.html
    <!-- 1. load the widget once (defines the custom element) -->
    <script
      src="https://relay.firstbyte.fast/embed/chat.js"
      async
    ></script>

    <!-- 2. drop the element; fetch the token from YOUR backend -->
    <clutchcall-chat
      id="support-chat"
      org="org_abc"
      title="Support"
      subtitle="We usually reply in a minute"
      position="bottom-right"
    ></clutchcall-chat>

    <script>
      // hand the element a fresh conversation-scoped token
      const el = document.getElementById("support-chat");
      el.tokenProvider = async () => {
        const r = await fetch("/api/chat-token", { method: "POST" });
        const { token } = await r.json();   // from mintChatToken() above
        return token;
      };
    </script>
    ```

    > **TIP:**
> Pass a `tokenProvider` function rather than a static `token` attribute. The
>     widget can then silently mint a new token when the token expires or when
>     the visitor starts a new conversation. A static `token="…"` attribute also
>     works for quick tests.

That is a working chat launcher. The flow named in the token (`support`) runs
on connect. It greets, collects, and queues, exactly as you built it in the
console.

## Present a form

Forms are **flow-driven**. A step in your flow presents a form. The step blocks
until the visitor submits a valid `form_result`. The widget renders the fields
and does client-side validation. It returns the structured result to the
engine. You write no front-end code. Add a form step to the flow that the token
points at:

```json flow: support (excerpt)
{
  "steps": [
    { "type": "announce", "text": "Hi! Before we start — a couple details." },
    {
      "type": "form",
      "id": "intake",
      "title": "How can we help?",
      "fields": [
        { "name": "email",  "label": "Work email", "type": "email", "required": true },
        { "name": "topic",  "label": "Topic", "type": "select",
          "options": ["Billing", "Technical", "Sales"], "required": true },
        { "name": "detail", "label": "Details", "type": "textarea" }
      ]
    },
    { "type": "queue", "skill": "support" }
  ]
}
```

The engine emits a `form` envelope. The widget renders it. On submit, the
widget publishes a `form_result` envelope. The flow validates the result before
it advances. The captured values are available to downstream steps and to the
agent who picks up.

> **NOTE:**
> For a custom (non-widget) client: you receive the `form` envelope on the
> `server` track. You then publish a matching `form_result` on the `client` track
> yourself. See the [envelope protocol](/modalities/chat/sdk-methods#forms).

## Route to a skill / queue

A `queue` step puts the conversation into a skill queue. It announces the queue
position while the visitor waits. Human agents, an AI text agent, or both can
staff the same skill. Routing picks a handler that is eligible and available.

**flow step: queue to a skill:**
```json
{ "type": "queue", "skill": "billing", "announcePosition": true }
```

**branch on a form answer, then queue:**
```json
{
  "type": "switch",
  "on": "intake.topic",
  "cases": {
    "Billing":   [ { "type": "queue", "skill": "billing" } ],
    "Technical": [ { "type": "queue", "skill": "tier1" } ],
    "Sales":     [ { "type": "queue", "skill": "sales" } ]
  }
}
```

You configure skills, staffing, and concurrency in the console. Concurrency is
the number of chats that one agent can hold at the same time. If no one accepts
a queued conversation, it follows your RONA / re-offer policy, the same as a
queued voice call.

## Attach an AI text agent

Point a queue step, or the whole flow, at an AI text agent to answer
automatically. The agent is the **same agent runtime as voice, in text mode**.
It has the same tools, knowledge, and DAG. Text turns drive it instead of
audio.

**flow: answer with an AI agent:**
```json
{
  "steps": [
    { "type": "announce", "text": "Hi! I'm an assistant — ask me anything." },
    { "type": "agent", "agent": "support-bot" }
  ]
}
```

**skip the flow, connect straight to an agent:**
```html
<clutchcall-chat
  org="org_abc"
  agent="support-bot"
  title="Ask the docs bot"
></clutchcall-chat>
```

The AI agent streams replies token-by-token as `message` envelopes. It can
raise a `typing` indicator between turns. When it hits a tool or a policy that
requires a person, it escalates. See the next recipe.

## Hand off to a human

Escalation is a flow transition. The AI agent, or a `switch` on its outcome,
routes the live conversation into a **human** skill queue. The conversation
keeps its id. When the human accepts, they see the visitor's history, the
captured form, and the AI transcript.

```json flow: AI first, human on escalate
{
  "steps": [
    { "type": "agent", "agent": "support-bot", "onEscalate": "to-human" }
  ],
  "labels": {
    "to-human": [
      { "type": "announce", "text": "Connecting you with a specialist…" },
      { "type": "queue", "skill": "support-humans" }
    ]
  }
}
```

  1. **The AI agent decides a human is needed**
Any tool, guardrail, or explicit "talk to a person" intent transitions the
    flow to the `to-human` label.
  2. **The conversation queues to a human skill**
The engine offers it in the Agent desk. A human agent can hold **several
    concurrent chats**, so pickup is fast. The engine re-offers unanswered
    offers per your RONA policy.
  3. **The human takes over in place**
The engine relays the agent's messages onto the same `server` track that
    the widget subscribes to. The visitor's chat window never reconnects and
    never loses scrollback.

> **NOTE:**
> The full agent-side experience lives in the console, not the widget. This
> includes offer acceptance, the multi-chat workstation, and what the human sees
> on pickup. Media escalation from a text chat to a voice/video call is on the
> roadmap. Today, handoff is text-to-text.

## Send a file attachment

Attachments ride **out of band**. Upload the raw bytes over HTTPS. The response
is an attachment meta. Then publish a normal `message` envelope that references
the meta. The bytes never travel through the real-time session. Only the small
meta does.

> **NOTE:**
> On the widget, this is **zero-code**. The paperclip button, drag-and-drop, and
> a multi-file picker (`<input accept>`) upload and attach for you. The recipe
> below is for **custom clients** that speak the
> [envelope protocol](/modalities/chat/sdk-methods) directly.

  1. **Upload the bytes, get an attachment meta**
`POST` the raw file bytes to the upload endpoint with the **session token**.
    This is the same conversation-scoped token that the widget or client holds.
    Put the file name in the `x-cc-filename` header. Set `Content-Type` to the
    file's MIME type. The endpoint permits only an allowed MIME list and a
    maximum size. It rejects all other uploads.

    
    **upload.ts (TypeScript):**
```ts
    // Returns the attachment meta to reference from a message envelope.
    async function uploadAttachment(token: string, file: File) {
      const res = await fetch("https://engine.firstbyte.fast/v1/chat/upload", {
        method: "POST",
        headers: {
          "authorization": `Bearer ${token}`, // conversation-scoped session token
          "x-cc-filename": file.name,
          "content-type": file.type,           // must be an allowed MIME type
        },
        body: file,                            // raw bytes — NOT multipart
      });
      if (!res.ok) throw new Error(`upload rejected: ${res.status}`); // MIME/size
      return res.json(); // { id, name, size, mime, url }
    }
    ```

    **upload.py (Python):**
```python
    import requests

    def upload_attachment(token: str, path: str, mime: str) -> dict:
        with open(path, "rb") as f:
            res = requests.post(
                "https://engine.firstbyte.fast/v1/chat/upload",
                headers={
                    "authorization": f"Bearer {token}",
                    "x-cc-filename": path.rsplit("/", 1)[-1],
                    "content-type": mime,
                },
                data=f,               # raw bytes
                timeout=30,
            )
        res.raise_for_status()        # 4xx if MIME/size not allowed
        return res.json()             # { id, name, size, mime, url }
    ```

    **curl:**
```bash
    # $CHAT_TOKEN is the conversation-scoped session token
    curl -sX POST https://engine.firstbyte.fast/v1/chat/upload \
      -H "authorization: Bearer $CHAT_TOKEN" \
      -H "x-cc-filename: quote.pdf" \
      -H "content-type: application/pdf" \
      --data-binary @quote.pdf
    # → {"id":"att_9f…","name":"quote.pdf","size":48213,
    #    "mime":"application/pdf","url":"https://engine.firstbyte.fast/v1/chat/attachment/…"}
    ```
    

    - **`x-cc-filename`** (`header`, required) — The original file name. The meta keeps it, and the download URL contains       it.
    - **`content-type`** (`header`, required) — The file's MIME type. It must be on the allowed list, or the endpoint       rejects the upload.

    - **`id`** (`string`) — The attachment id. It is unique in the conversation.
    - **`name`** (`string`) — The file name as uploaded.
    - **`size`** (`number`) — The byte size that the engine recorded.
    - **`mime`** (`string`) — The stored MIME type.
    - **`url`** (`string`) — The download URL for the bytes. It resolves to       `GET /v1/chat/attachment/<conv>/<id>/<name>`. Object storage sits behind       it.

  2. **Publish a message envelope that carries the meta**
Send a normal `message` envelope. Set `data.attachments` to an array of the
    metas that you received. `text` is optional. An **attachment-only** message
    can leave it empty. To attach several files, upload each file, then list
    all the metas.

    
    **send.ts (TypeScript):**
```ts
    // `publish` is your client-track publisher — see the envelope protocol page.
    async function sendFile(token: string, file: File, text = "") {
      const meta = await uploadAttachment(token, file); // step 1
      await publish({
        kind: "message",
        data: {
          text,                 // "" for an attachment-only message
          attachments: [meta],  // one or more { id, name, size, mime, url }
        },
      });
    }
    ```

    **envelope on the wire:**
```json
    {
      "kind": "message",
      "data": {
        "text": "Here's the quote you asked for.",
        "attachments": [
          { "id": "att_9f…", "name": "quote.pdf", "size": 48213,
            "mime": "application/pdf", "url": "https://engine.firstbyte.fast/v1/chat/attachment/…" }
        ]
      }
    }
    ```
    

    > **NOTE:**
> The engine emits a `chat.file.uploaded` event `{ role, count }`. It records
>     `"[file: <names>]"` in the transcript. Thus an attached AI text agent knows
>     what files the parties shared, although it cannot read the bytes.

## React to a message

Every `message` envelope carries a **`mid`**. This is an author-owned message
id, unique in the conversation. The engine stamps it and relays it. Reactions,
and quoted replies via `reply_to`, address a message by its `mid`. To react,
publish a `reaction` envelope with `data { op, emoji, target_mid }`.

> **NOTE:**
> On the widget, this is **zero-code**. The hover quick-reaction strip and the
> emoji picker publish these envelopes for you. Reaction pills show live counts.
> The recipe below is for **custom clients**.

**react.ts (TypeScript):**
```ts
// Add or remove an emoji reaction on a message, addressed by its mid.
async function react(mid: string, emoji: string, op: "add" | "remove" = "add") {
  await publish({
    kind: "reaction",
    data: { op, emoji, target_mid: mid },
  });
}

// e.g. react to the last agent message you received
react(lastMessage.data.mid, "👍");          // add
react(lastMessage.data.mid, "👍", "remove"); // undo
```

**envelope on the wire:**
```json
{ "kind": "reaction", "data": { "op": "add", "emoji": "👍", "target_mid": "m_7f3a…" } }
```

- **`op`** (`'add' | 'remove'`, required) — Add the reaction, or remove one that you added before.
- **`emoji`** (`string`, required) — The emoji to attach, e.g. `"👍"`, `"🎉"`, `"❤️"`.
- **`target_mid`** (`string`, required) — The `mid` of the target message.

The engine relays the reaction to the other side. It emits a `chat.reaction`
event `{ role }`. Reaction envelopes are their own `kind`. The full set is
`message | typing | form | form_result | reaction | event`. See the
[envelope protocol](/modalities/chat/sdk-methods).

## Custom theme

Match the widget to your brand. Use attributes for the common options. Use CSS
`::part()` selectors for deep styling. The widget is a real custom element, so
your page CSS can style its shadow parts.

**attributes (quick):**
```html
<clutchcall-chat
  org="org_abc"
  mode="dark"
  accent="#6d28d9"
  radius="16"
  position="bottom-left"
  launcher-icon="chat"
></clutchcall-chat>
```

**::part() (fine-grained):**
```css
clutchcall-chat::part(launcher) {
  background: linear-gradient(135deg, #6d28d9, #4f46e5);
  box-shadow: 0 8px 24px rgba(79, 70, 229, .35);
}
clutchcall-chat::part(header) { font-weight: 600; }
clutchcall-chat::part(bubble-agent)   { background: #f5f3ff; }
clutchcall-chat::part(bubble-visitor) { background: #4f46e5; color: #fff; }
```

- **`mode`** (`'light' | 'dark' | 'auto'`, default `auto`) — Color scheme. `auto` follows the visitor's `prefers-color-scheme`.
- **`accent`** (`string (CSS color)`) — The primary accent color for the launcher, the buttons, and the visitor   bubbles.
- **`radius`** (`number`) — The corner radius in pixels for the panel and the bubbles.
- **`position`** (`'bottom-right' | 'bottom-left'`, default `bottom-right`) — The docked corner for the floating launcher.

## Mount programmatically

You can skip the declarative markup and create the widget from JavaScript. Use
this for SPAs, consent-gated loading, or a panel that opens from your own
button. The element exposes `open()`, `close()`, and a `config` object.

```js
// create and configure the element after the loader has run
const chat = document.createElement("clutchcall-chat");

chat.config = {
  org: "org_abc",
  flow: "support",
  title: "Support",
  mode: "auto",
  accent: "#6d28d9",
  tokenProvider: async () => {
    const r = await fetch("/api/chat-token", { method: "POST" });
    return (await r.json()).token;
  },
};

document.body.appendChild(chat);

// open the panel from your own CTA instead of the floating launcher
document.querySelector("#help-button")
  ?.addEventListener("click", () => chat.open());

// react to lifecycle in your app (analytics, unread badges, …)
chat.addEventListener("conversation", (e) => {
  console.log("conversation id:", e.detail.conversation);
});
chat.addEventListener("message", (e) => {
  console.log(e.detail.role, e.detail.text); // 'visitor' | 'agent' | 'ai'
});
```

> **TIP:**
> To guard the load behind consent: inject the
> `https://relay.firstbyte.fast/embed/chat.js` script tag only after the visitor
> opts in. Then run the snippet above when the tag fires `load`. Nothing connects
> until the element mounts and receives a token.

> **NOTE:**
> For a fully custom UI with no widget, drop to the wire. Subscribe to the
> `server` track. Publish `client` envelopes yourself over raw MoQT.
> [Widget & API](/modalities/chat/sdk-methods) documents the message, typing,
> form, and event envelope shapes.

## Related

  - **[Widget & envelope protocol](/modalities/chat/sdk-methods)** — Every widget attribute and the JSON envelope wire for custom clients.
  - **[Chat overview](/modalities/chat/details)** — How conversations, tracks, flows, and agents fit together.
  - **[End-to-end recipes](/modalities/chat/recipes)** — Full builds: a support widget with AI-to-human handoff and lead capture.
  - **[Deployment models](/modalities/voice/concepts/deployment-models)** — Where the engine runs and how to make chat highly available.
