First embedded chat
Use the harness module loader and an authenticated backend ticket endpoint. The current client contract uses mountWidget, not a standalone component tag.
Open Publish & history โ Embed. Replace the example host, workspace ID and independently supplied public-key ring below with that snippet. The examples contain placeholders; they are not credentials.
Before mounting
- Accept your invitation and sign in to your workspace.
- Keep a tenant server key in your backend secret manager.
- Publish your branding, active web configuration and exact Allowed sites origins.
- Implement the authenticated backend ticket endpoint.
Backend bootstrap contract
Your backend keeps the vt_ mint token and returns { ticket, nonce, signedConfig } with Cache-Control: no-store. The ticket starts with vf_. The loader redeems it from the approved host Origin, keeps the resulting scoped vs_ session in memory and creates a user-owned thread. A console session or another user's thread is not needed. The publicKeys ring must be trusted independently of the returned signed configuration.
Mount the chat
<div id="client-chat" style="height:640px"></div>const HARNESS_ORIGIN = 'https://chat.example.com';
// Copy this independent key ring and workspace ID from your console snippet.
const PUBLIC_KEYS = { 'SUPPLIED_KEY_ID': 'SUPPLIED_PUBLIC_KEY_HEX' };
const { mountWidget } = await import(`${HARNESS_ORIGIN}/embed/loader.mjs`);
const chat = await mountWidget({
organizationId: 'YOUR_ORG_ID',
baseUrl: HARNESS_ORIGIN,
publicKeys: PUBLIC_KEYS,
container: document.getElementById('client-chat'),
getSession: async () => {
const response = await fetch('/chat/session', {
method: 'POST', credentials: 'same-origin', cache: 'no-store',
});
if (!response.ok) throw new Error('Chat could not open.');
return response.json(); // { ticket, nonce, signedConfig }
},
});
// On logout or account change:
// chat.destroy();Renewal and logout
getSession is called again before expiry and when access needs renewal. It must return a fresh ticket for the same authenticated user and tenant. Reject callers who have signed out; unmount on logout or account change. Keep keys and sessions out of URLs, HTML attributes, persistent client storage and logs.
Limits and host errors
Integrations โ Allowed sites โ Daily chat limit publishes a tenant-wide ceiling from 1 to 1,000,000 turns per UTC day, default 10,000 (daily_cap_reached). A user has one tenth of that daily cap, at least one, and 20 turns per fixed UTC minute; a mint session permits 10 turns per minute. Keys and session refresh do not reset shared counters. Each backend token has a $10 ceiling and each turn reserves $2 before settlement. Voice has a shared 20-request burst refilling at 20 per minute and 2,000 requests per tenant per UTC day.
Text chat permits 8 simultaneous turns per user (USER_CONCURRENT_TURN_LIMIT) and 64 simultaneous turns per workspace (TENANT_CONCURRENT_TURN_LIMIT); both are HTTP 429 with Retry-After: 5. Service-wide capacity can return HTTP 503 TURN_CAPACITY. Wait for an active answer to finish and preserve the draft. A turn at 15 minutes ends with TURN_ABSOLUTE_TIMEOUT; an interrupted turn recovered after a restart reports TURN_INTERRUPTED. Reconnect to check its status and receipt before starting another billed turn.
Pass onError to mountWidget to render your own neutral availability or limit message. See the complete typed refusal reference. Balance refusals can arrive over HTTP or inside a stream; never automatically retry billed turns.
onError: ({ code, requestId, retryAfterSecs }) => {
showChatAvailability({ code, requestId, retryAfterSecs });
}Permissions and failures
The frame's media delegation does not grant site or device permission. Enable voice and attachments only when your tenant and host permissions allow them. Handle backend or origin refusal as a visible error. Check Stop, settled receipts, low balance, reload and logout on an approved HTTPS site.
Continue with React Native and other platforms or support.