Endpoint
POST/api/public/v1/textvoice/session
Authenticates with your Bearer key, gates on the textvoice scope, mints an HMAC-signed short-lived token, returns the ready-to-use WebSocket URL.
Request
Body (JSON, optional)
| Field | Type | Notes |
|---|---|---|
flow_id | string | Optional. The UUID of a conversation flow you've designed in the dashboard. Omit for a free-form conversation against the default agent. |
Response · 200
JSON
| Field | Type | Notes |
|---|---|---|
ws_url | string | WebSocket URL with the signed public_token already attached as a query param. Connect to this directly. |
ws_token | string | Same token as in the URL, exposed separately so you can move it into a header if you prefer. |
expires_at | ISO 8601 string | When the token stops being accepted by the WS gateway. Open the socket immediately. |
surface | "textvoice" | "voicebot" | Echoes the surface you called — useful for client-side switching. |
WebSocket protocol
Connect to the returned ws_url. The token is a query parameter — do not strip it. Frames are a mix of JSON control messages and binary audio.
WebSocket frames (server → client)
| Field | Type | Notes |
|---|---|---|
{ type: "transcript_partial", text } | JSON | Streaming ASR partials — overwrite the previous partial in your UI. |
{ type: "transcript_final", text } | JSON | Finalised ASR turn. |
{ type: "agent_speech", text } | JSON | What the agent said — for transcript display. |
audio frames | binary | Opus-encoded TTS audio frames. Pipe into a WebAudio decoder. |
{ type: "end" } | JSON | Backend will close the socket cleanly after this frame. |
Errors
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request | Body was malformed JSON or exceeded the 4 KB cap. |
| 401 | missing_api_key | Authorization header absent or not a vv_live_… Bearer token. |
| 401 | invalid_api_key | Key not found, revoked, or disabled. |
| 402 | insufficient_balance | Wallet is empty and autopay is not active. Top up or enable autopay. |
| 403 | scope_denied | Key exists but is not authorized for this surface. |
| 429 | rate_limited | Per-key budget exceeded. See X-RateLimit-* headers. |
| 500 | server_error | We failed. Retry with exponential backoff. |
| 502 | upstream_error | Upstream backend (LiveKit / meeting-agent) failed. Retry. |
Examples
curl -X POST https://www.vaanilabs.in/api/public/v1/textvoice/session \
-H "Authorization: Bearer $VAANI_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "flow_id": "your-flow-uuid-or-omit" }'
# Response 200
# {
# "ws_url": "wss://textvoice.vaanilabs.in/browser-stream?public_token=...",
# "ws_token": "<short-lived JWT>",
# "expires_at": "2026-04-25T12:34:56.000Z",
# "surface": "textvoice"
# }const apiKey = process.env.VAANI_API_KEY;
const res = await fetch(
"https://www.vaanilabs.in/api/public/v1/textvoice/session",
{
method: "POST",
headers: {
"Authorization": `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ flow_id: flowId }),
},
);
if (!res.ok) throw new Error(`gateway ${res.status}`);
const { ws_url } = await res.json();
const ws = new WebSocket(ws_url);
ws.onopen = () => console.log("connected");
ws.onmessage = (e) => console.log("frame", e.data);
ws.onclose = () => console.log("closed");import os, asyncio, requests, websockets
API_KEY = os.environ["VAANI_API_KEY"]
r = requests.post(
"https://www.vaanilabs.in/api/public/v1/textvoice/session",
headers={"Authorization": f"Bearer {API_KEY}"},
json={"flow_id": flow_id},
timeout=10,
)
r.raise_for_status()
session = r.json()
async def stream():
async with websockets.connect(session["ws_url"]) as ws:
async for frame in ws:
print("frame", frame)
asyncio.run(stream())