- 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 meanstrue.
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.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
Everymessage 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 normalmessage 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.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.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 envelopekind. 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 }
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 namespacechat/<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.Related
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.

