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.
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.
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.
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.

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.
string
required
The workspace or org id that owns the conversation. It scopes the chat namespace (chat/<org>/…) and the minted token.
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.
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.
string
default:"relay.clutchcall.dev"
The relay host that the widget connects to. Override it only for a self-hosted or region-pinned relay.
'auto' | 'light' | 'dark'
default:"auto"
Color scheme. auto follows the visitor’s prefers-color-scheme.
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.
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.
'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.
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.
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.
number
The protocol version. Currently 1. Reject envelopes with a version that you do not understand.
number
A monotonic sequence number per track. Use it to order and de-duplicate envelopes. A gap means that you missed an object.
number
The author timestamp, in Unix epoch milliseconds.
'message' | 'typing' | 'form' | 'form_result' | 'reaction' | 'event'
The envelope type. It determines the shape of data (below).
'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).
object
The payload for the given kind. See the table below.

data by kind

{ 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). text is the body. Optional attachments carry file references. Optional reply_to quotes an earlier message. The transcript shows the message.
{ state: 'start' | 'stop' }
The typing indicator. Send start/stop as the visitor types. Show the peer’s indicator when you receive it.
{ 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.
{ 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.
{ 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.
{ 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 and quoted replies address a target message by its 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.
{ 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.
On the widget, quoted replies work with no code. The mid/reply_to fields matter when you 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.
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.
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.
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.
object
The meta that the upload returns and that data.attachments carries.
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 identifies it. The engine relays the reaction to the other side and emits a chat.reaction event.
{ op: 'add' | 'remove'; emoji: string; target_mid: string }
The engine emits a chat.reaction event { role } when it relays a reaction.
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: 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.
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.

Chat overview

How the flow engine, AI text agents, human handoff, forms, and skill queues fit together.

Chat cookbook

Copy-pasteable recipes: embed on a page, present a form, route to a skill, hand off to a human.

Chat recipes

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

CSTA CTI

Drive third-party call control (makeCall, holdCall, transferCall) over the same engine connection from an ISV/SDK client.