websocket agent talks to a browser directly. There is no telephony provider, no answer webhook, and no phone number — the page opens one WebSocket to the runtime and exchanges Pipecat protobuf frames over it. This page covers the URL, the wire format, and a minimal client.
The runtime dispatches on the agent’s
agent_category, not on the path. Browser and telephony clients use the same route. See Agents and agent categories.The URL
One route, declared inapps/runtime/routes/agent.py:
VOICE_SERVER_BASE_URL is the public base URL of the runtime, set in the root .env. The runtime rewrites the scheme itself when it builds telephony URLs (https:// becomes wss://, http:// becomes ws://, in apps/runtime/constants.py) — a browser client must do the same when deriving the URL from an HTTP origin.
Locally, with the default port:
call_id query parameter attaches the session to a call log you registered in advance:
The websocket agent category
The runtime loads the agent from the API and branches immediately:agent_categoryiswebsocket—run_websocket_bot()runs, usingProtobufFrameSerializeratWEBSOCKET_SAMPLE_RATE, and nostartevent is expected.agent_categoryistelephony— the runtime blocks waiting for a providerstartJSON message and closes with code1008if the first message is anything else.
start frame it has no reason to send. Create the agent with agent_category: "websocket" and omit telephony_provider. GET /api/v1/agents/{agent_id} tells you which category an existing agent has.
Protobuf and RTVI frames
The transport isProtobufFrameSerializer from Pipecat — every message in both directions is a binary protobuf Frame. The schema the dashboard compiles at runtime, and which the runtime speaks:
audio frames carrying signed 16-bit little-endian PCM. You receive audio frames to play, and transcription, text, and message frames to render. MessageFrame.data is a JSON string carrying RTVI events.
The supported client library is @pipecat-ai/websocket-transport (with @pipecat-ai/client-js / @pipecat-ai/client-react). The dashboard uses that stack — see Browser test calls and frontend/src/lib/pipecat/createBrowserClient.ts.
Sample rate
Browser sessions run atWEBSOCKET_SAMPLE_RATE, which defaults to 16000 in .env.example. Telephony runs at SAMPLE_RATE, default 8000. Set the same rate on your client recorder/player and in the sample_rate field of every AudioRawFrame you send. Mismatched rates do not error — they produce audio at the wrong pitch and speed.
A minimal client
Prefer the official packages over hand-rolled protobuf:ws(s)://{host}/agent/...) after POST /calls/web. For a standalone page talking to the runtime directly, use the runtime host as above.
Authentication and CORS
Treat the runtime as an internal service and put a reverse proxy in front of it. Terminate TLS there, restrict by origin or source address, and add your own authentication if the page is public. See Security hardening. CORS does not apply to WebSockets, so there is nothing to configure for the media connection. The REST calls your page makes alongside it — fetching the agent list, for example — hit the API on:8000, whose CORSMiddleware in apps/api/app/main.py is configured with allow_origins=["*"]. That is convenient for local development and too permissive for production.
What is recorded
Browser sessions are logged like telephony calls. On connect the runtime resolves acall_id:
- If the URL carries
?call_id=, it loads that call log and checks two things —call_typemust beweb, andagent_idmust match the path. A mismatch is logged and the id discarded. - Otherwise, or after a discard, it registers a new one with
POST /api/v1/calls/web.
call_id in hand the transcript writer and recording handlers are registered, exactly as for telephony.
Artifacts land under the same paths —
voicera-calls/{org_id}/{call_id}/ — and are fetched through the same authenticated API routes.
Pre-register only when your page needs the call_id before the session begins:
transcription, text, and message frames still give you the turn-by-turn text live, which is what you render during the call.