Quickstart

Five-minute integration

You'll need three values from the voice_orch console: a deployment public ID, a deployment key, and the base URL of this backend. Step 1 walks you through getting each one; steps 2 and 3 wire them into your own product.

1. Get your credentials

Three env vars feed every integration. All three come from the console. Never hand-edit them, and never put any of them in browser code.

Env varWhat it isWhere in the console
VOICE_ORCH_BASE_URLOrigin of this voice_orch deploy (no path).The URL bar of the console you're reading this from. Copy everything before /docs.
VOICE_ORCH_DEPLOYMENT_IDPublic ID of the deployment (e.g. dep_xyz123). Stable; never rotates.Agents → pick your workspace → pick your agent →Deployments tab → select a deployment → copy the value next toPublic deployment ID.
VOICE_ORCH_DEPLOYMENT_KEYSecret that authorizes minting connect tokens. Acts like a service API key. Shown once, only at issue time.Same deployment screen → click Rotate key. A fresh key appears underIssued deployment key in this browser tab only. Copy it immediately and store in your secrets manager. Refreshing the page wipes it. If you lose it, rotate again (which revokes the previous one).

Don't have a deployment yet? In the console, open or create an agent, publish a version, then click Create deployment. The first key is issued automatically. The same one-time-view rule applies, so copy it before navigating away. While you're there, add your site's origin (e.g. https://acme.com) toAllowed origins. Without it the WebSocket will reject the browser handshake with a 1008 policy-violation close.

Once you have the three values, drop them into your server's env / secret manager:

.env (server-side only, NEVER ship to the browser)dotenv
VOICE_ORCH_BASE_URL=https://voice.acme.com
VOICE_ORCH_DEPLOYMENT_ID=dep_xyz123
VOICE_ORCH_DEPLOYMENT_KEY=voa_8f3a...

2. Mint a token on your server

The widget asks your backend for an ephemeral connect token; your backend proxies to voice_orch with the deployment key. The key never ships to the browser. Below are two drop-in handlers (Node and Python) that you can wire to any framework that supports POST endpoints.

server/voice-token.ts (Express / Next.js API route)Node.js
import express from 'express';

const router = express.Router();

// Held server-side only. Pull from your secrets store; never log it.
const DEPLOYMENT_PUBLIC_ID = process.env.VOICE_ORCH_DEPLOYMENT_ID!;
const DEPLOYMENT_KEY      = process.env.VOICE_ORCH_DEPLOYMENT_KEY!;
const VOICE_ORCH_BASE     = process.env.VOICE_ORCH_BASE_URL!;  // e.g. https://voice.acme.com

router.post('/api/voice-token', async (req, res) => {
  // Forward only fields you trust. The widget sends endUserId + metadataJson;
  // ignore or override them here per your auth model. For example, derive
  // endUserId from the signed-in session rather than trusting the browser.
  const body = {
    endUserId: req.user?.id ?? req.body?.endUserId,
    metadataJson: { source: 'widget', ...(req.body?.metadataJson ?? {}) },
  };

  const upstream = await fetch(
    `${VOICE_ORCH_BASE}/api/public/deployments/${DEPLOYMENT_PUBLIC_ID}/token`,
    {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${DEPLOYMENT_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(body),
    },
  );

  if (!upstream.ok) {
    return res.status(upstream.status).json({ error: await upstream.text() });
  }

  const { connectToken, websocketUrl } = await upstream.json();
  // Pass through only what the widget needs. Never expose the deployment key.
  res.json({ connectToken, websocketUrl });
});

export default router;
server/voice_token.py (FastAPI)Python
import os
import httpx
from fastapi import APIRouter, HTTPException, Request

router = APIRouter()

DEPLOYMENT_PUBLIC_ID = os.environ['VOICE_ORCH_DEPLOYMENT_ID']
DEPLOYMENT_KEY       = os.environ['VOICE_ORCH_DEPLOYMENT_KEY']
VOICE_ORCH_BASE      = os.environ['VOICE_ORCH_BASE_URL']  # e.g. https://voice.acme.com


@router.post('/api/voice-token')
async def mint_voice_token(request: Request):
    body = await request.json()
    # Override endUserId server-side from the signed-in session if you have one.
    forward = {
        'endUserId': getattr(request.state, 'user_id', None) or body.get('endUserId'),
        'metadataJson': {'source': 'widget', **(body.get('metadataJson') or {})},
    }

    async with httpx.AsyncClient(timeout=10.0) as client:
        upstream = await client.post(
            f'{VOICE_ORCH_BASE}/api/public/deployments/{DEPLOYMENT_PUBLIC_ID}/token',
            headers={
                'Authorization': f'Bearer {DEPLOYMENT_KEY}',
                'Content-Type': 'application/json',
            },
            json=forward,
        )

    if upstream.status_code != 200:
        raise HTTPException(status_code=upstream.status_code, detail=upstream.text)

    payload = upstream.json()
    # Pass through only what the widget needs. Never expose the deployment key.
    return {'connectToken': payload['connectToken'], 'websocketUrl': payload['websocketUrl']}

Tip. The voice_orch token endpoint also accepts a sessionConfigfield that lets you override prompt, voice, or VAD settings per call. Whitelist it carefully. See the production checklist for the full allowlist.

3. Drop the widget on your page

The widget is one file. Load it from your voice_orch deployment and place the<voice-orch-mic> tag wherever you want the mic to appear. It works in any framework. The tag is a standard Web Component.

anywhere on your pageHTML
<!-- Load the bundle once per page. ~23 KB minified. -->
<script src="https://voice.acme.com/widget.js" defer></script>

<!-- Drop this anywhere. Sized via the --voiceorch-size CSS variable. -->
<voice-orch-mic
  token-endpoint="/api/voice-token"
  end-user-id="user_42"
></voice-orch-mic>

That's it for the happy path. Click the mic, allow the browser permission prompt, and you should hear the agent's first response. The widget handles mic capture, TTS playback, barge-in, and reconnects automatically.

4. Listen to events (optional)

For most teams the default UI is enough. If you want to surface transcripts in your own chat panel, react to tool calls, or instrument analytics, the widget emits bubbling CustomEvents on the element itself.

vanilla / React-agnosticJavaScript
const mic = document.querySelector('voice-orch-mic');

mic.addEventListener('voiceorch:status', (e) => {
  // { status: 'idle' | 'connecting' | 'live' | 'error', message?: string }
  console.log('status →', e.detail.status);
});

mic.addEventListener('voiceorch:transcript', (e) => {
  // { role: 'user' | 'assistant' | 'system', text: string }
  appendToChat(e.detail.role, e.detail.text);
});

mic.addEventListener('voiceorch:tool-call', (e) => {
  // { name: string, arguments: string | object, raw: <event> }
  showToolBadge(e.detail.name);
});

mic.addEventListener('voiceorch:error', (e) => {
  // { message: string, code: string }
  toast.error(e.detail.message);
});

Full event reference is on the widget reference page. If you want the raw runtime events (the same ones the backend emits over the WebSocket), listen forvoiceorch:event instead.

What can go wrong

  • 403 from your token endpoint. Most likely the deployment's Allowed origins list does not include the page's origin. Edit the deployment in the console.
  • "Connect token does not match this deployment." The signing key rotated. Re-mint a token and reconnect; old tokens are rejected.
  • Mic button does nothing on first click. Browsers require a user gesture to start audio. The first click is the gesture. If you triggered .start() from JS without a click, the browser blocks. Either move the call into a click handler or set auto-start="true" and accept that the first user gesture still has to happen.
  • Hydration warnings in dev mode. The widget loads via classic <script>, not as an SSR-rendered component. If you mount it inside React, render it inside a useEffect or with dynamic(..., { ssr: false }).