Skip to main content
The realtime WebSocket protocol for Omni, the all-in-one voice agent model: a hybrid speech-to-speech engine with a fused LLM brain that hears, reasons, calls your tools, grounds answers in your knowledge base, and speaks back in emotion-aware voices, all over this one socket. Omni runs on a single engine tier, so there is one protocol and one surface for agentic voice.
  • Endpoint: wss://api.pyai.com/v1/omni
  • Scope: omni:session (or the omni:* wildcard)
  • Status: GA
Migrating an older Omni client? The former chat URL was discontinued on August 13, 2026. Use the Omni endpoint migration guide before applying this canonical protocol reference.
Some browser-serving revisions also send a 0x03 advisory with exactly {"event":"transcript","role":"assistant","text":"…","final":true}. This describes the text submitted for speech synthesis; it does not confirm that the complete reply was played. Caller transcripts still use 0x02. For voice-quality evaluation, compare this advisory with a transcription of the captured audio. A machine-readable AsyncAPI 3.0 definition of this protocol ships alongside the OpenAPI contract at contracts/omni-asyncapi.yaml.
Field stability. Connect params, auth, the configure frame, the kb_endpoint callback, close codes, and metering are stable. Server→client lifecycle event payloads below document the confirmed envelope; individual fields marked provisional may gain keys. Branch only on event and ignore unknown fields. The official SDKs track these for you.
Frames are binary and type-prefixed, not text frames. Every server → client message is a binary frame whose first byte is a type tag: 0x01 = agent audio (PCM16), 0x02 = caller transcript (plain UTF-8 text), 0x03 = control/lifecycle (JSON keyed on event). A client that treats all binary as audio and parses only text frames for events will play control frames as a glitch and never see hello / session_started / transcript. Demux on the first byte (the official SDKs do this for you):

1. Connect

Omni is zero-state by default, there is nothing to create first. The session is authorized by your key’s organization; the agent’s behavior can travel in the first configure frame below. A managed Agent profile is an optional convenience.

2. Auth

Browsers can’t set Authorization on a WebSocket upgrade, so pass the key as a subprotocol (browser-safe):
Server-side clients may instead append ?api_key=pyai_live_... to the URL. The key is validated at the edge and swapped for the internal engine credential, the customer key never reaches the engine. Don’t put the key in any other query param.

3. Audio frames

Send microphone audio as binary WebSocket messages in the negotiated format/rate (PCM16 little-endian), each prefixed with the 0x01 type tag. Receive the agent’s speech the same way, 0x01-prefixed binary frames you strip and play out as they arrive at the sample rate declared by hello.audio_out. Send caller frames continuously; the engine handles turn detection and barge-in server-side. For browser clients with a turn-0 consent_line or greeting, “continuously” means keeping the audio clock alive with digital-silence frames while that protected opening plays. Do not uplink microphone or speaker/self-audio until the playback graph is ready and the opening queue has drained. Then restore real mic samples; on later replies, preserve normal barge-in after a short echo-cancellation warm-up. Browser AEC alone is not an opening-audio gate.
The 0x01 tag is mandatory and omitting it fails without a client error. The engine demuxes every client frame on its first byte; an untagged PCM16 frame is dropped and counted internally, but no error frame is sent. You get a clean handshake, zero transcripts, and an agent that idles into “are you still there” — the session looks healthy and is simply deaf. The official SDKs tag for you.

Turn finalization: no commit frame

Omni has no client commit, EOF, done, or end-of-turn frame. Server-side turn detection advances while caller-audio frames arrive. Keep sending real-time 0x01 frames, including PCM16 zeroes while the caller is quiet, until reply audio or the next turn boundary. Digital zeroes are valid silence. A fixed trailing burst is not a commit signal: if a late transcript delta lands near the end of that burst, stopping all frames can leave no later tick on which to close the turn. File/WAV probes should send 20 ms zero frames until reply audio (with a bounded timeout), not send 800 ms once and then go idle. {"type":"session_ending"} and the WebSocket close end the session; they do not finalize a caller turn or request a reply. {"type":"commit"} is a Hear streaming-STT control, not an Omni control.

Transcript body

Every live 0x02 frame contains a non-empty UTF-8 text delta for the current caller turn. It is not JSON:
Coalesce successive deltas into one partial caller row. The live wire carries no role or finality bit; applications may finalize the row when agent audio or a later turn boundary arrives. Official SDKs normalize each delta to {event:"transcript", role:"user", text, final:false, mode:"delta"} and retain bounded direct-JSON support only for older bridges.

4. Configure frame

Omni accepts complete behavior per session. Immediately after the upgrade, the client sends one JSON configure control frame carrying the agent’s behavior for this call. Control frames are keyed on type and carried as 0x03 || utf8_json, not as bare WebSocket text.

Fields: live vs roadmap

Send only live fields for behavior you expect today. The gateway forwards unknown keys verbatim, but the engine ignores roadmap fields until they ship, sending them is a no-op, not an error. For per-call context (e.g. a user’s chart/profile), use persona plus the kb_endpoint callback rather than a roadmap field.
configured.voice_instruct_supported reports whether the selected synthesis path accepts delivery direction. In the current engine, English Natural supports it; Standard voices, including Elise and Gwen, do not. A saved or accepted voice_instruct value alone does not establish support. A missing capability field on an older engine means unknown, not supported. If delivery direction is required, require this field to be true before starting the call. Support means the direction can be passed to synthesis, not that the resulting audio is guaranteed to match a subjective style. If English Natural synthesis falls back to Standard during a call, Omni sends { "event": "voice_capabilities", "voice_tier": "standard", "voice_instruct_supported": false, "reason": "synthesis_fallback" }. Use this event to update the capability state from configured.
Voice ids ending in _en_in are Indian-English voices, not Hindi voices. They remain compatible with an English-active session; they do not make that session Hindi. Hindi uses the Standard tier; see the pricing page for current tier treatment.

Agent profiles (POST /v1/agents)

Store persona, role, voice delivery, greeting message, recording disclosure, and tools once; connect with session_label={agent_id} so the engine loads them from your stored agent profile (no need to repeat greeting in configure unless overriding).
Connect:
Playback order when recordings are on: consent_line → greeting (turn 0) → conversation. Full walkthrough: Agent greeting messages · REST: POST /v1/agents.

4a. Function calling (tools[])

Omni supports function calling on the live engine. Declare tools in the configure frame:
Client-loop (default). Omit any URL on the tool. When the brain selects a tool, the engine emits:
A write that needs caller confirmation first emits { "event": "tool_confirmation_required", "name": "…", "reason": "…" } instead of running the tool. After the caller confirms, the next turn may emit tool_call (client-loop) or invoke a registered server/hosted tool. Run a client-loop function in your app and reply on the same WebSocket:
On errors, return { "type": "tool_result", "call_id": "…", "error": "…" }. Timeouts: tools are load-bearing (unlike kb_endpoint grounding). Default per-tool budget is ~5 s (up to ~15 s). Long-running calls may trigger a brief spoken filler while the engine waits. Results over ~6 KB are truncated. Server tools: register a webhook with POST /v1/tools and bind it to the agent (PUT /v1/agents/{id}/tools). Do not put endpoint or webhook_url on the configure frame, that is rejected with { "event": "error", "code": "unsupported_tool_transport" } and the configure is not applied. Hosted catalog tools are enabled by name. Full guide: Omni function calling.

4.1 Call-control frames

Call-control tools run in engine mode on phone calls. Omni decides when a tool should fire and emits a 0x03 control frame. The transport carrying the phone leg must perform the carrier action. Every frame is JSON keyed on event. call_id identifies the engine tool invocation; tool arguments are spread alongside it:
On a self-hosted transport, enabling one of these tools does not perform the carrier operation by itself. Implement the corresponding event handler first. The Twilio and FreeSWITCH guides show complete switches. Browser and in-app sessions have no phone leg, so these events are unavailable.

5. kb_endpoint grounding callback

If you set kb_endpoint, the engine calls your endpoint once per user turn to fetch grounding facts. This call comes from PyAI’s engine, not the browser. Request (engine → your endpoint):
Response (your endpoint → engine): return grounding facts for the turn. A ready-to-inject context string and/or structured passages both work; keep it small and fast. Budget: the call has a hard ~300 ms timeout and is fail-open, on timeout or any error the engine proceeds with empty facts and never blocks the turn. Treat it as best-effort augmentation; keep it well under budget.
There is no documented retry of the callback, design it as a single best-effort call. session_label is how you route per-call context (e.g. look up the caller’s chart/profile by the tag you connected with).

6. Session lifecycle events (server → client)

On connect you receive hello followed by session_started, then turn and flush events interleaved with audio and plain-text 0x02 transcript deltas, ending in session_end. Every server JSON body is keyed only on event; the transcript body is not JSON. A flush with reason: "turn_merge" can include cancelled_turn, identifying a revoked generation when the caller resumes. Clear playback as usual. Preserve all turn_begin, audio and transcript events: a generation start is not proof of a spoken reply, and the cancellation receipt does not erase any received output.

7. Control frames (client → server)

8. Close codes

The server uses standard WebSocket close codes plus PyAI-specific application codes. Treat 4xxx-class application closes as non-retryable (fix the request); treat 1011-class closes as retryable with backoff. A malformed session_label is rejected before the upgrade as 400 invalid_session_label (an HTTP error, not a WS close).

9. Reconnect & retry

There is no mid-call session resume. A dropped socket means the session is over, reconnecting opens a new session and you must send a fresh configure frame. In-flight turn state is not preserved by PyAI.
Recommended pattern:
  • Retry on 1011 and 4429 with exponential backoff; do not retry 4401 / 4403 (fix the key/scope first).
  • Keep per-call state (the session_label, persona/context, a short running summary) in your backend so a reconnect can re-prime configure / kb_endpoint and continue gracefully.

10. Metering

Omni sessions report omni.minutes from session wall-clock duration. Check the pricing page for the current Omni and managed telephony rates, included usage, and billing rules. Realtime WebSocket sessions do not carry an x-pyai-units response header (that’s HTTP-only), reconcile realtime usage from your call records and usage data.

See also

Authentication

Key handling and the WS subprotocol.

Errors & limits

Rate, concurrency, and the error catalog.

Telephony audio (8 kHz)

μ-law ↔ PCM16 at 8 kHz for phone legs.

Language support

What’s GA vs roadmap per language.