Guides

Web voice sessions

Run a voice agent inside your web app: your server mints a session, the browser connects to wss://rtc.vollo.io with the LiveKit JS SDK, and the same published workflows answer — no phone number involved.

The token flow#

Browsers never hold your API key. The flow is always three steps:

  1. The browser asks your backend to start a voice session.
  2. Your backend calls POST /api/v1/sessions with its API key (scope calls:write) and returns the short-lived join_token to the browser.
  3. The browser connects to wss://rtc.vollo.io with that token. An agent worker joins the same room and the workflow starts.

Minting a session#

cURL / shellPOST /sessions — server-side only
curl -X POST https://api.vollo.io/api/v1/sessions \
  -H "Authorization: Bearer pk_live_8f3KJd92mA4qL7Zx1RttVWpc" \
  -H "Content-Type: application/json" \
  -d '{
    "project_id": "9b2e7c1a-4d3f-4e8a-9c5b-2f7d1e6a8b3c",
    "workflow_id": "7f3a9d2e-8c1b-4f6a-b5d4-e2c9a1f7b8d0",
    "variables": { "customer_name": "Lina" },
    "metadata": { "user_id": "u_582" }
  }'
JSON201 Created
{
  "data": {
    "id": "5e8b3c1d-9f2a-4d7e-b6c8-1a4f7d2e9b3c",
    "channel": "web",
    "status": "created",
    "join_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.…",
    "rtc_url": "wss://rtc.vollo.io",
    "expires_at": "2026-08-18T09:22:44Z",
    "workflow_version": 3,
    "created_at": "2026-08-18T09:12:44Z"
  }
}

Notes:

Connecting from the browser#

Install the LiveKit client (npm install livekit-client), then:

JavaScriptConnect and talk
import { Room, RoomEvent, Track } from 'livekit-client';

// 1. Get a session from your own backend (which calls POST /sessions)
const res = await fetch('/api/voice-session', { method: 'POST' });
const { join_token, rtc_url } = await res.json();

// 2. Connect to Vollo's media plane
const room = new Room();
await room.connect(rtc_url, join_token); // wss://rtc.vollo.io

// 3. Play the agent's audio when it arrives
room.on(RoomEvent.TrackSubscribed, (track) => {
  if (track.kind === Track.Kind.Audio) {
    track.attach(); // creates and plays an <audio> element
  }
});

// 4. Publish the microphone — the workflow starts once audio is up
await room.localParticipant.setMicrophoneEnabled(true);

// Later: hang up from the client side
// await room.disconnect();
Browser autoplay

Call room.connect() from a user gesture (a “Start call” button). Browsers block both microphone capture and audio autoplay outside user-initiated contexts.

Session lifecycle#

StatusMeaning
createdMinted, waiting for the browser to connect. Expires unused at expires_at.
activeParticipant connected; the workflow is executing.
endedTerminal. Reached when the workflow hits Hangup/End, the browser disconnects, the session hits the workflow's max_duration_seconds, or you force it with DELETE /sessions/{id}.
expiredTerminal. Never connected before expires_at.

On every transition to ended, a session.ended webhook fires with duration and end reason, followed by workflow.completed or workflow.failed for the execution.

Transcripts#

Every session (web and phone) produces a conversation — the ordered transcript of user, assistant, system, and tool messages with per-message latency. Fetch it after the session ends:

cURL / shellFetch the transcript
curl "https://api.vollo.io/api/v1/conversations?session_id=5e8b3c1d-9f2a-4d7e-b6c8-1a4f7d2e9b3c" \
  -H "Authorization: Bearer pk_live_8f3KJd92mA4qL7Zx1RttVWpc"
JSON200 OK (excerpt)
{
  "data": [
    {
      "id": "0c3f7a9e-2d5b-4c8e-9a1f-6b4d8e2c5a7f",
      "session_id": "5e8b3c1d-9f2a-4d7e-b6c8-1a4f7d2e9b3c",
      "messages": [
        { "sequence": 1, "role": "assistant", "content": "Hi Lina! How can I help today?", "spoken_at": "2026-08-18T09:13:02Z", "latency_ms": 420 },
        { "sequence": 2, "role": "user", "content": "I want to check my order status.", "spoken_at": "2026-08-18T09:13:09Z" }
      ]
    }
  ]
}

Transcripts stream into the conversation while the session is live, so polling mid-call works too. For push-based delivery, listen for session.ended and fetch once.

Testing tip#

The builder's test-call panel uses this exact mechanism with channel: "test" — sessions it creates appear in GET /sessions like any other, which makes end-to-end debugging (webhooks included) easy before you write a line of frontend code.