You add live chat to a site by dropping in one web component. It mints a namespace-scoped token, opens a persistent connection (QUIC first, WSS fallback), and drives the conversation with the same flow/step engine 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 <telequick-chat> web component and its config attributes.
  • The envelope protocol — the JSON wire format on the chat tracks, so you can build a custom client instead of using the widget.
Chat is widget/JS-first today. There is no polyglot chat SDK (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 unversioned beyond the v field — treat it as the low-level contract, not a supported SDK.

Embed the widget

1

Load the component

Include the widget script once, anywhere on the page. It registers the custom element and does nothing until you place the tag.
<script type="module" src="https://relay.telequick.dev/chat/widget.js"></script>
2

Place the tag

Drop the element where you want the launcher to appear. Only org is strictly required; the widget fetches a per-visitor token at runtime.
<telequick-chat
  org="org_abc"
  token-url="/api/chat-token"
  theme="auto">
</telequick-chat>
3

Mint tokens from your backend

The widget does not embed a secret. It calls token-url on your origin, which returns a short-lived, namespace-scoped chat token (scoped to chat/<org>/<conv>) minted by the control-plane API. Keep your API key server-side; return only the token to the browser.
// your backend — POST /api/chat-token
const res = await fetch("https://portal.telequick.dev/v1/chat/tokens", {
  method: "POST",
  headers: { authorization: `Bearer ${process.env.TELEQUICK_CREDENTIALS}` },
  body: JSON.stringify({ org: "org_abc" }),
});
const { token } = await res.json();
return Response.json({ token }); // browser passes this to the widget

Config attributes

Attributes are set on the tag (kebab-case) or via the DOM property of the same name. Booleans follow HTML rules — present means true.
org
string
required
Workspace / org id the conversation belongs to. Scopes the chat namespace (chat/<org>/…) and the minted token.
token-url
string
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 so credentials never ship to the browser.
token
string
A pre-minted chat token, if you would rather fetch it yourself and set it directly. Mutually exclusive with token-url. Short-lived — set it before the session opens.
relay
string
default:"relay.telequick.dev"
Relay host the widget connects to. Override only for a self-hosted or region-pinned relay.
theme
'auto' | 'light' | 'dark'
default:"auto"
Color scheme. auto follows the visitor’s prefers-color-scheme.
conversation
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.
greeting
string
Optional opening line rendered before the flow runs. Display-only — the flow engine still drives the actual conversation.
launcher
'bubble' | 'inline'
default:"bubble"
bubble floats a launcher button; inline mounts the panel in the tag’s own box (embed it in a page section).
The attribute surface is still settling. org plus a token source (token-url or token) is the firm contract; the display attributes above are convenience wrappers and may change name. Pin the widget script version in production and re-check this page after upgrades.

Programmatic control

Grab the element and drive it from JavaScript for open/close, prefilled context, and lifecycle hooks.
const chat = document.querySelector("telequick-chat")!;

chat.open();                       // open the panel
chat.close();
chat.setContext({ plan: "pro", accountId: "acct_42" }); // attach visitor metadata
chat.addEventListener("conversation", (e) => {
  console.log("conversation id:", e.detail.conversation);
});
chat.addEventListener("message", (e) => {
  console.log(e.detail.role, e.detail.data.text); // mirror of server envelopes
});
The DOM events are a thin mirror of the envelope protocol — each one carries the underlying envelope in event.detail. If you only need to observe or embellish 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, one per transport object. It is the contract you implement when you build a custom client.
{
  "v": 1,
  "seq": 128,
  "ts": 1752345600123,
  "kind": "message",
  "role": "visitor",
  "data": { "mid": "m_71be04", "text": "Hi, I need help with my order" }
}
v
number
Protocol version. Currently 1. Reject envelopes with a version you do not understand.
seq
number
Monotonic sequence number per track. Use it to order and de-duplicate; gaps mean you missed an object.
ts
number
Author timestamp, Unix epoch milliseconds.
kind
'message' | 'typing' | 'form' | 'form_result' | 'reaction' | 'event'
What the envelope is. Determines the shape of data (below).
role
'visitor' | 'agent' | 'system'
Who authored it. visitor is the end user, agent is a human or AI text agent, system is the flow engine (announcements, queue updates, forms).
data
object
Kind-specific payload. See the table below.

data by kind

message
{ 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. Rendered in the transcript.
typing
{ state: 'start' | 'stop' }
Typing indicator. Emit start/stop as the visitor types; render the peer’s indicator when you receive it.
form
{ formId: string; title?: string; fields: Field[] }
The engine is presenting a form and will block the flow until it receives a matching form_result. Each Field has { name, label, type, required, options? }. Render it and collect input.
form_result
{ formId: string; values: Record<string,unknown> }
Your reply to a form. Send it (role visitor) with the collected values. The engine validates against the form definition and unblocks — or re-presents the form on a validation failure.
reaction
{ op: 'add' | 'remove'; emoji: string; target_mid: string }
Add or remove an emoji reaction on the message identified by target_mid. The engine relays it to the other side and emits a chat.reaction event. See Reactions.
event
{ 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 (AI resumes after a human idles out), closed. Treat unknown events as advisory.

Message identity and replies

Every message envelope carries a mid — an author-owned message id, unique within the conversation. The engine stamps and relays it, and it is how one message points at another: both reactions and quoted replies address a target message by its mid.
mid
string
Stable identifier for a message, unique within the conversation. Present on every message envelope. Use it as the React/render key and as the target for reactions and quoted replies.
reply_to
{ mid: string; preview: string }
Present when the message is a quoted reply. mid is the message being quoted; preview is a short snippet of that message for rendering the quote inline. Omit it for a normal (non-reply) message.
{
  "v": 1, "seq": 141, "ts": 1752345600890,
  "kind": "message", "role": "agent",
  "data": {
    "mid": "m_9f3a2c",
    "text": "Yes — I can see order #4471 shipped this morning.",
    "reply_to": { "mid": "m_71be04", "preview": "where's my order?" }
  }
}
On the widget, quoted replies are wired already — no code. The mid/reply_to fields matter when you build a custom client or need to understand the wire.

File attachments

A message can carry files. The bytes travel out-of-band over HTTP, not over the chat tracks — you upload the file to the control-plane API first, then publish a normal message envelope that references the returned attachment meta. This keeps large transfers off the realtime tracks.
On the widget this is fully built in: a paperclip attach button, drag-and-drop, and a multi-file picker. The flow below is for custom clients and for understanding the wire.
1

Upload the bytes

POST the raw file bytes to the upload endpoint with the session token. The filename rides in the x-cc-filename header and the Content-Type is the file’s MIME type. The response is the attachment meta.
const res = await fetch("https://engine.telequick.dev/v1/chat/upload", {
  method: "POST",
  headers: {
    authorization: `Bearer ${token}`,      // the session token
    "x-cc-filename": file.name,
    "content-type": file.type,             // e.g. image/png
  },
  body: file,                              // raw file bytes
});
const attachment = await res.json();
// { id, name, size, mime, url }
with open(path, "rb") as f:
    res = httpx.post(
        "https://engine.telequick.dev/v1/chat/upload",
        headers={
            "authorization": f"Bearer {token}",
            "x-cc-filename": name,
            "content-type": mime,
        },
        content=f.read(),                  # raw file bytes
    )
attachment = res.json()  # { id, name, size, mime, url }
Allowed MIME types and a maximum size are enforced. Uploads outside the allow-list or over the size limit are rejected — validate client-side and surface the error to the user.
2

Publish a message that references it

Send a normal message envelope with data.attachments set to an array of the metas you got back. An attachment-only message is allowed — text may be empty.
await client.send(JSON.stringify({
  v: 1, seq: seq++, ts: Date.now(),
  kind: "message", role: "visitor",
  data: { mid: newMid(), text: "", attachments: [attachment] },
}));
3

Download on the other side

Each attachment url resolves to the download endpoint. The other side fetches it with the session token to retrieve the bytes.
GET https://engine.telequick.dev/v1/chat/attachment/<conv>/<id>/<name>
Attachment
object
The meta returned by the upload and carried in data.attachments.
When an upload lands, the engine emits a chat.file.uploaded event { role, count }, and the transcript records a [file: names] line so an AI text agent has context that a file was shared even though it never sees the raw bytes.

Reactions

A reaction is its own envelope kind. It adds or removes an emoji on a message identified by that message’s mid. The engine relays the reaction to the other side and emits a chat.reaction event.
data
{ op: 'add' | 'remove'; emoji: string; target_mid: string }
// react to a message
await client.send(JSON.stringify({
  v: 1, seq: seq++, ts: Date.now(),
  kind: "reaction", role: "visitor",
  data: { op: "add", emoji: "👍", target_mid: "m_9f3a2c" },
}));
The engine emits a chat.reaction event { role } when a reaction is relayed.
On the widget, reactions are built in — an emoji picker, a hover quick-reaction strip, and reaction pills with counts — so 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 and exchange envelopes yourself.

Track layout

A conversation lives under the namespace chat/<org>/<conv> with three tracks:
TrackDirectionYou (custom client)
clientvisitor → enginePublish your envelopes here
serverengine → visitorSubscribe — this is your only read
agenthuman agent → engineDo not touch — the engine relays it to server
The engine folds the human agent track into server, so a client keeps a single subscription (server) and a single publish (client) regardless of 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 — the same connection the widget uses.
2

Publish to `client`, subscribe to `server`

Announce/publish chat/<org>/<conv>/client and subscribe chat/<org>/<conv>/server. Each transport object 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 and reply with a form_result. React to event envelopes for queue position and handoff state.
// Pseudocode over your MoQT/WebTransport client — one JSON object per envelope.
const base = `chat/${org}/${conv}`;
const session = await connectChat({ relay: "relay.telequick.dev", token });

const client = await session.publish(`${base}/client`);
const server = await session.subscribe(`${base}/server`);

// send a message (stamp your own author-owned mid)
let seq = 0;
await client.send(JSON.stringify({
  v: 1, seq: seq++, ts: Date.now(),
  kind: "message", role: "visitor",
  data: { mid: newMid(), text: "Hi, I need help with my order" },
}));

// receive + handle
for await (const obj of server) {
  const env = JSON.parse(obj);
  switch (env.kind) {
    case "message":  render(env.role, env.data); break; // mid, text, attachments, reply_to
    case "reaction": applyReaction(env.data); break;    // { op, emoji, target_mid }
    case "typing":   setPeerTyping(env.data.state === "start"); break;
    case "event":    handleEvent(env.data); break; // queued/position/agent_joined/closed
    case "form": {
      const values = await collect(env.data.fields);
      await client.send(JSON.stringify({
        v: 1, seq: seq++, ts: Date.now(),
        kind: "form_result", role: "visitor",
        data: { formId: env.data.formId, values },
      }));
      break;
    }
  }
}
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 and 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.