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 ClutchCall Voice: announcements, waits with queue position, forms, and skill queues. It also supports AI text agents and human handoff.
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 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.
string
required
Your organization or workspace id. It scopes the conversation namespace.
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.
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.
index.html
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:
flow: support (excerpt)
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.
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.

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.
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.
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.
flow: AI first, human on escalate
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 on the agent console. 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.
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.
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 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.
header
required
The original file name. The meta keeps it, and the download URL contains it.
header
required
The file’s MIME type. It must be on the allowed list, or the endpoint rejects the upload.
string
The attachment id. It is unique in the conversation.
string
The file name as uploaded.
number
The byte size that the engine recorded.
string
The stored MIME type.
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.
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 }.
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.
'add' | 'remove'
required
Add the reaction, or remove one that you added before.
string
required
The emoji to attach, e.g. "👍", "🎉", "❤️".
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.

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.
'light' | 'dark' | 'auto'
default:"auto"
Color scheme. auto follows the visitor’s prefers-color-scheme.
string (CSS color)
The primary accent color for the launcher, the buttons, and the visitor bubbles.
number
The corner radius in pixels for the panel and the bubbles.
'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.
To guard the load behind consent: inject the https://relay.clutchcall.dev/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.
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 documents the message, typing, form, and event envelope shapes.

Widget & envelope protocol

Every widget attribute and the JSON envelope wire for custom clients.

Chat overview

How conversations, tracks, flows, and agents fit together.

End-to-end recipes

Full builds: a support widget with AI-to-human handoff and lead capture.

Deployment models

Where the engine runs and how to make chat highly available.