Two complete, runnable builds. The first drops a support widget on a page that greets visitors with an AI text agent and escalates to a human with the full transcript in front of them. The second presents a form and routes the conversation by what the visitor answered. Both use the same three pieces: a flow you design once in the console, a server endpoint that mints a short-lived, namespace-scoped token, and the web component you drop on the page. The widget opens a single connection (QUIC first, secure WebSocket fallback) and never sees your API key.
Chat is web-component + JavaScript first today — there is no native mobile/polyglot chat SDK. To drive chat from another platform, speak the envelope protocol 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>:
TrackDirectionWho uses it
clientvisitor → enginethe widget publishes visitor messages here
serverengine → visitorthe widget subscribes here (its only read)
agenthuman agent → enginerelayed onto server, so the widget has one read
Messages are JSON envelopes, one per MoQT object:
{ "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.

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

A visitor opens your site, the widget greets them with an AI text agent, and if they ask for a person the conversation is offered to a human agent who sees the entire transcript. Because the human joins the same conversation (same tracks), no history is lost.
1

Design the flow in the console

In the console’s visual flow builder — the same step engine that powers voice IVR — build a flow named support:
  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 (the agent runtime running in text mode). It answers on the server track.
  3. Escalation branch. When the visitor’s intent is “talk to a human” (or 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, with RONA (redirect-on-no-answer) re-offer if the first agent doesn’t pick up.
The human agent’s portal shows the offered conversation with the full transcript already loaded, so they read the context before their first reply. One human agent can hold several conversations at once (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 and asks the control-plane API to mint a token scoped to exactly one conversation’s tracks.
import { Chat } from "@telequick/sdk/chat";

const chat = new Chat({
  baseUrl: "https://portal.telequick.dev",   // control-plane API
  apiKey:  process.env.TELEQUICK_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 }
});
import os
from telequick import Chat

chat = Chat(
    base_url="https://portal.telequick.dev",
    api_key=os.environ["TELEQUICK_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.telequick.dev
    }
The returned token carries an ns claim of chat/acme/<conv>/* — the widget can only publish and subscribe within 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 lowercased + -chat. It fetches a token from token-endpoint, opens the connection, and runs the support flow.
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.telequick.dev/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>

    <telequick-chat
      org="acme"
      flow="support"
      token-endpoint="/api/chat/token"
      transport="auto"
      title="Acme Support"
      subtitle="Typically replies in seconds"
      position="bottom-right">
    </telequick-chat>
  </body>
</html>
org
string
required
Your workspace id. Must match the token 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. Defaults to the workspace’s default chat flow.
transport
'auto' | 'wss'
default:"auto"
auto tries QUIC and falls back to a secure WebSocket; wss forces the fallback (useful 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 on the agent portal in the console. Offered conversations appear with the transcript pre-loaded; an agent accepts, and their replies flow onto the agent track, which the engine relays to server so the widget renders them 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 so the widget can show a status line, then the human’s messages arrive as ordinary message envelopes:
{ "v": 1, "seq": 42, "ts": 1733900123, "kind": "event", "role": "system",
  "data": { "type": "handoff", "to": "human", "queue": "support", "position": 2 } }
{ "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." } }
The human sees the transcript — every visitor and AI turn on the conversation. What does not transfer is the AI agent’s hidden state (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

Here the flow presents a form, blocks on a validated result, and then branches: high-budget leads go straight to a human sales queue; everyone else continues 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 await a validated result. The engine sends a form envelope and will 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 — e.g. budget == "$5k+"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)

Same endpoint as Recipe 1, pointed at the other flow:
app.post("/api/lead/token", async (req, res) => {
  const grant = await chat.widgets.mintToken({
    org:        "acme",
    flow:       "lead-capture",
    ttlSeconds: 1800,
  });
  res.json(grant);
});
@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

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.telequick.dev/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>

    <telequick-chat
      org="acme"
      flow="lead-capture"
      token-endpoint="/api/lead/token"
      transport="auto"
      title="Get a demo"
      auto-open="true">
    </telequick-chat>
  </body>
</html>
auto-open="true" opens the panel on load so the form appears immediately.

The form round-trip

The engine sends a form envelope on the server track; the widget renders the fields and, on submit, publishes a form_result on the client track. The flow validates it and continues.
{ "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"
  } }
{ "v": 1, "seq": 4, "ts": 1733901020, "kind": "form_result", "role": "user",
  "data": {
    "id": "lead",
    "values": { "email": "dana@acme.io", "company": "Acme", "budget": "$5k+" }
  } }
Because the branch above tests budget, this visitor is queued to the human sales skill; a $1k–5k visitor would continue with the AI text agent instead. Validation is enforced server-side — if email is malformed or budget is missing, the flow re-presents the form rather than advancing.
The form fields are collected by your engine flow, not posted to a third party. Treat the mint endpoint like any credential surface: rate-limit it and set a short ttlSeconds so a leaked token expires quickly.

Drive it from a custom client

If you can’t use the web component (a native app, a kiosk, a server-to-server bot), open the same three tracks over a raw MoQT client and speak the envelope protocol yourself: publish message / form_result on client, subscribe server, and honor typing / event / form. The wire schema and a minimal client are in Widget & API.

Widget & API

Every widget attribute, the full envelope schema, and a raw-MoQT client.

Chat overview

How chat rides MoQT and shares the flow engine with voice.

Cookbook

Short, single-purpose snippets: embed, form, route, hand off.

Authentication

How short-lived, namespace-scoped tokens are minted and verified.