# Recipes

> End-to-end chat builds: a support widget with AI-to-human handoff and a lead-capture form that routes by result.

This page contains two complete, runnable builds. The first build puts a
support widget on a page. The widget greets visitors with an AI text agent. It
escalates to a human, who sees the full transcript. The second build presents a
form and routes the conversation by the visitor's answers.

Both builds use the same three pieces:

1. A **flow** that you design once in the console.
2. A **server endpoint** that mints a short-lived, namespace-scoped token.
3. The **web component** that you put on the page.

The widget opens a single connection (QUIC first, secure WebSocket fallback).
It never sees your API key.

> **NOTE:**
> Chat is **web-component + JavaScript first** today. There is no native mobile
> or polyglot chat SDK. To drive chat from another platform, speak the
> [envelope protocol](/modalities/chat/sdk-methods) over a raw MoQT client. The
> server-side token mint shown here is part of the control-plane API.

## How a conversation is wired

Every conversation lives under three MoQT tracks at `chat/<org>/<conv>`:

| Track    | Direction              | Who uses it                                          |
| -------- | ---------------------- | ---------------------------------------------------- |
| `client` | visitor → engine       | the widget **publishes** visitor messages here       |
| `server` | engine → visitor       | the widget **subscribes** here (its only read)       |
| `agent`  | human agent → engine   | the engine relays it onto `server`, so the widget has one read |

Messages are JSON envelopes, one per MoQT object:

```json
{ "v": 1, "seq": 7, "ts": 1733900000, "kind": "message", "role": "agent",
  "data": { "text": "Hi! How can I help?" } }
```

`kind` is one of `message | typing | form | form_result | event`. The full
schema is in [Widget & API](/modalities/chat/sdk-methods).

---

## Recipe 1 — Support widget with AI-to-human handoff

A visitor opens your site. The widget greets them with an AI text agent. If
they ask for a person, the engine offers the conversation to a human agent. The
human agent sees the entire transcript. The human joins the **same**
conversation, with the same tracks, so no history is lost.

  1. **Design the flow in the console**
Build a flow named `support` in the console's visual flow builder. This is
    the same step engine that powers voice IVR:

    1. **Announce** a greeting (`"You're chatting with our assistant."`).
    2. **Queue to skill → AI text agent.** Point the step at a text
       conversation DAG. This is the agent runtime in text mode. It answers on
       the `server` track.
    3. **Escalation branch.** When the visitor's intent is "talk to a human",
       or when the AI calls a `handoff` tool, **queue to skill → human**. The
       visitor keeps chatting on the same conversation. The engine offers it to
       an available human agent. If the first agent does not pick up, the
       engine re-offers it (RONA, redirect-on-no-answer).

    The human agent's portal shows the offered conversation **with the full
    transcript** already loaded. The agent reads the context before their first
    reply. One human agent can hold several conversations at the same time
    (multi-chat concurrency).

  2. **Mint a token on your server**
The widget calls a URL on **your** server to get a short-lived token. Your
    server holds the API key. It asks the control-plane API to mint a token
    scoped to exactly one conversation's tracks.

    
    **server.ts (TypeScript):**
```ts
    import { Chat } from "clutchcall_connect/chat";

    const chat = new Chat({
      baseUrl: "https://portal.firstbyte.fast",   // control-plane API
      apiKey:  process.env.CLUTCHCALL_API_KEY,   // never leaves the server
    });

    // POST /api/chat/token  → { token, conversationId, relayUrl }
    app.post("/api/chat/token", async (req, res) => {
      const grant = await chat.widgets.mintToken({
        org:        "acme",           // your workspace id
        flow:       "support",        // the flow to run
        ttlSeconds: 3600,
        visitor:    { id: req.cookies.uid },   // optional attributes
      });
      res.json(grant);   // { token, conversationId, relayUrl }
    });
    ```

    **server.py (Python):**
```python
    import os
    from clutchcall-agents import Chat

    chat = Chat(
        base_url="https://portal.firstbyte.fast",
        api_key=os.environ["CLUTCHCALL_API_KEY"],
    )

    # POST /api/chat/token  -> { token, conversationId, relayUrl }
    @app.post("/api/chat/token")
    def chat_token():
        grant = chat.widgets.mint_token(
            org="acme",
            flow="support",
            ttl_seconds=3600,
            visitor={"id": request.cookies.get("uid")},
        )
        return {
            "token":          grant.token,
            "conversationId": grant.conversation_id,
            "relayUrl":       grant.relay_url,   # relay.firstbyte.fast
        }
    ```
    

    The returned token carries an `ns` claim of `chat/acme/<conv>/*`. The
    widget can publish and subscribe only in that one conversation.

  3. **Drop the web component on the page**
Load the widget script and place the element. The tag is your brand name in
    lowercase plus `-chat`. The element fetches a token from `token-endpoint`.
    It opens the connection and runs the `support` flow.

    ```html index.html
    <!doctype html>
    <html lang="en">
      <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <title>Acme — Support</title>
        <script type="module"
                src="https://cdn.firstbyte.fast/chat/v1/widget.js"></script>
      </head>
      <body>
        <main>
          <h1>How can we help?</h1>
          <p>Chat with us — the assistant answers instantly and can hand you to
             a specialist.</p>
        </main>

        <clutchcall-chat
          org="acme"
          flow="support"
          token-endpoint="/api/chat/token"
          transport="auto"
          title="Acme Support"
          subtitle="Typically replies in seconds"
          position="bottom-right">
        </clutchcall-chat>
      </body>
    </html>
    ```

    - **`org`** (`string`, required) — Your workspace id. It must match the token that the endpoint mints.
    - **`token-endpoint`** (`string`, required) — A URL on your server that returns `{ token, conversationId, relayUrl }`.       The widget calls it when it first opens.
    - **`flow`** (`string`) — The flow to run. If omitted, the widget uses the workspace's default chat       flow.
    - **`transport`** (`'auto' | 'wss'`, default `auto`) — `auto` tries QUIC first and falls back to a secure WebSocket. `wss`       forces the fallback. Use `wss` behind proxies that block UDP.
    - **`title / subtitle / position`** (`string`) — Header text and launcher placement (`bottom-right`, `bottom-left`).

  4. **Staff the human side**
Human agents sign in to the **Agent desk** workspace of the Contact Center console. Offered
    conversations appear with the transcript pre-loaded. An agent accepts.
    Their replies flow onto the `agent` track. The engine relays that track to
    `server`, and the widget renders the replies inline. Set each agent's
    concurrent chat capacity so one person can cover several conversations.

### What the handoff looks like on the wire

When the flow crosses the escalation branch, the engine emits an `event`
envelope on the `server` track. The widget can then show a status line. After
that, the human's messages arrive as ordinary `message` envelopes:

```json
{ "v": 1, "seq": 42, "ts": 1733900123, "kind": "event", "role": "system",
  "data": { "type": "handoff", "to": "human", "queue": "support", "position": 2 } }
```

```json
{ "v": 1, "seq": 55, "ts": 1733900140, "kind": "message", "role": "agent",
  "data": { "text": "Hi, this is Dana — I've read the thread. Let's fix that." } }
```

> **NOTE:**
> The human sees the **transcript**: every visitor turn and every AI turn on the
> conversation. The AI agent's hidden state does not transfer. That state is its
> tool-call scratchpad. The human works from the visible conversation, exactly as
> the visitor sees it.

---

## Recipe 2 — Lead-capture form that routes by result

In this recipe, the flow presents a form. It **blocks** until a validated
result arrives, and then it branches. High-budget leads go straight to a human
sales queue. All other visitors continue with the AI text agent. You write no
form HTML. The widget renders a `form` envelope natively and returns a
`form_result`.

  1. **Design the form and the branch**
Build a flow named `lead-capture`:

    1. **Announce** a one-line intro.
    2. **Present a form and wait for a validated result.** The engine sends a
       `form` envelope. It does not advance until the widget returns a
       `form_result` that passes the field validation you set (required
       fields, email format).
    3. **Branch on the result.** Route by a field value. Example: if
       `budget == "$5k+"`, then **queue to skill → human** (sales). Otherwise,
       **queue to skill → AI text agent** to answer questions and book a demo.

  2. **Mint the token (flow = lead-capture)**
Use the same endpoint as Recipe 1, pointed at the other flow:

    
    **server.ts (TypeScript):**
```ts
    app.post("/api/lead/token", async (req, res) => {
      const grant = await chat.widgets.mintToken({
        org:        "acme",
        flow:       "lead-capture",
        ttlSeconds: 1800,
      });
      res.json(grant);
    });
    ```

    **server.py (Python):**
```python
    @app.post("/api/lead/token")
    def lead_token():
        grant = chat.widgets.mint_token(
            org="acme", flow="lead-capture", ttl_seconds=1800,
        )
        return {
            "token":          grant.token,
            "conversationId": grant.conversation_id,
            "relayUrl":       grant.relay_url,
        }
    ```

  3. **Embed the widget**
```html landing.html
    <!doctype html>
    <html lang="en">
      <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <title>Acme — Get a demo</title>
        <script type="module"
                src="https://cdn.firstbyte.fast/chat/v1/widget.js"></script>
      </head>
      <body>
        <main>
          <h1>See Acme in action</h1>
          <p>Answer a couple of questions and we'll route you to the right team.</p>
        </main>

        <clutchcall-chat
          org="acme"
          flow="lead-capture"
          token-endpoint="/api/lead/token"
          transport="auto"
          title="Get a demo"
          auto-open="true">
        </clutchcall-chat>
      </body>
    </html>
    ```

    `auto-open="true"` opens the panel on load. The form then appears
    immediately.

### The form round-trip

The engine sends a `form` envelope on the `server` track. The widget renders
the fields. On submit, the widget publishes a `form_result` on the `client`
track. The flow validates the result and continues.

**server → widget (form):**
```json
{ "v": 1, "seq": 3, "ts": 1733901000, "kind": "form", "role": "agent",
  "data": {
    "id": "lead",
    "title": "Tell us about your project",
    "fields": [
      { "name": "email",   "label": "Work email", "type": "email",  "required": true },
      { "name": "company", "label": "Company",     "type": "text" },
      { "name": "budget",  "label": "Monthly budget", "type": "select",
        "options": ["<$1k", "$1k–5k", "$5k+"], "required": true }
    ],
    "submit": "Continue"
  } }
```

**widget → server (form_result):**
```json
{ "v": 1, "seq": 4, "ts": 1733901020, "kind": "form_result", "role": "user",
  "data": {
    "id": "lead",
    "values": { "email": "dana@acme.io", "company": "Acme", "budget": "$5k+" }
  } }
```

The branch above tests `budget`, so the flow queues this visitor to the human
sales skill. A `$1k–5k` visitor continues with the AI text agent instead. The
server enforces validation. If `email` is malformed or `budget` is missing, the
flow presents the form again and does not advance.

> **WARNING:**
> Your engine flow collects the form fields. The widget does not post them to a
> third party. Treat the mint endpoint like any credential surface. Rate-limit
> it. Set a short `ttlSeconds` so a leaked token expires quickly.

---

## Drive it from a custom client

Sometimes you cannot use the web component, for example in a native app, a
kiosk, or a server-to-server bot. In that case, open the same three tracks over
a raw MoQT client and speak the envelope protocol yourself:

1. Publish `message` / `form_result` on `client`.
2. Subscribe to `server`.
3. Honor `typing` / `event` / `form`.

The wire schema and a minimal client are in
[Widget & API](/modalities/chat/sdk-methods).

## Related

  - **[Widget & API](/modalities/chat/sdk-methods)** — Every widget attribute, the full envelope schema, and a raw-MoQT client.
  - **[Chat overview](/modalities/chat/details)** — How chat rides MoQT and shares the flow engine with voice.
  - **[Cookbook](/modalities/chat/cookbook)** — Short, single-purpose snippets: embed, form, route, hand off.
  - **[Authentication](/concepts/authentication)** — How short-lived, namespace-scoped tokens are minted and checked.
