Skip to main content

Orbit Voice Protocol (OVP)

Design preview — the wire format below is defined and tested; the live gateway does not yet speak it. This page is the target specification for OVP. The typed events and binary-frame codec ship today in the @devotel/voice package and are covered by conformance tests, so you can build and validate against the wire format now. The production voice bridge at wss://voice.orbit.devotel.io/voice/ws/{session_id} currently runs an earlier audio pipeline whose frame layout and audio sample rate differ from the spec on this page — it does not yet negotiate the orbit-voice/1.0 subprotocol or emit these events. Do not point a client at the live gateway expecting this protocol until this banner is removed. The endpoint, hostname, and JWT-authenticated upgrade described below are the intended shape of the service, not its current behaviour.
OVP is a bidirectional WebSocket protocol that connects a SIP call directly to an AI voice agent built on Orbit. Where Twilio ConversationRelay overloads a small handful of events and buries tool calls in the text channel, OVP gives every concern — audio, transcripts, LLM tokens, tool invocations, interrupts, handoffs, quality, cost — its own typed event. Base endpoint: wss://voice.orbit.devotel.io/voice/ws/{session_id}?token=<jwt> Subprotocol: orbit-voice/1.0 Audio format: PCM 16-bit mono 16kHz little-endian, 20ms frames Event wire format: UTF-8 JSON with type discriminator

Why a new protocol

Twilio’s ConversationRelay is functional. We talked to teams running it in production and found three sharp edges that we could close with a clean-sheet design:
  1. prompt is two events welded together — sometimes “the user said this”, sometimes “send this to the LLM”. Devs have to inspect heuristics on each frame to know which one they’re looking at.
  2. Tool calls are unobservable — they ride the same text channel as the LLM’s spoken output. You cannot subscribe to “show me every tool the agent invoked, with arguments and results, in order”.
  3. One interrupt for three different semantics — caller barge-in, programmatic cancel, and human takeover all collapse onto one primitive. They should drive different LLM-context truncation policies.
OVP starts from those problems and adds first-class events for everything else the platform already tracks internally — quality metrics, latency breakdowns, running cost, human-handoff lifecycle, mid-call voice/model swaps.

Connection lifecycle

1. Open the WebSocket

The session id and its signed token are issued together when the call session is created — a browser softphone receives them from its session-start response, and the inbound SIP path receives them from the dialplan. The token travels in the token query parameter (not an Authorization header) so the gateway can authenticate the upgrade before the WebSocket handshake completes.
The token is a short-lived HS256 JWT. It is bound to a single session, expires within 5 minutes of issue, and carries tenant_id, session_id, and the call context. It is minted server-side as part of session creation; there is no public per-agent token endpoint today. If the subprotocol header is missing or wrong, the handshake fails with HTTP 400 and an OVP_SUBPROTOCOL_MISMATCH body. If the JWT is bad, the handshake completes but the server immediately sends an error event with OVP_AUTH_FAILED and closes.

2. Server sends session.start

First frame from the server. Includes the protocol version, agent config, audio invariants, and the set of tools the LLM may call. Clients should store the session_id — it appears on every subsequent event.

3. Bidirectional audio + events

  • Binary frames carry PCM samples in both directions. Each frame starts with a 12-byte header (see below).
  • Text frames carry JSON events. Every event has type, seq, ts, session_id.
Sequence numbers (seq) are monotonic per direction. The server’s outbound counter and the client’s outbound counter increment independently. Servers must reject seq regressions with OVP_SEQ_REGRESSION to defend against replay.

4. Resume on drop

If the WebSocket drops within 30 seconds, the client may reconnect and send session.resume with the last seq it observed from the server. The server replays missed events and resumes the agent in place. Twilio drops the call.

5. End

Either the caller hangs up, the agent decides the call is over, the developer programmatically cancels, or a fatal error fires. The server emits session.end with a reason and final stats, then closes the WS with code 1000.

Binary audio frame format

Every binary WebSocket frame carries one 20ms PCM chunk prefixed by a 12-byte header.
Total frame size: 652 bytes. Why our own header instead of raw PCM? Because the agent pod needs to know about packet loss, silence, and DTMF overlay without a separate side channel — and the SBC’s RTP timestamp is the canonical clock for jitter-buffer math on the agent side.

Event reference

Every JSON event extends the envelope:
Direction column: S→C = server emits, C→S = client emits, = both.

Session lifecycle

session.start (server → client)

session.update (client → server)

Mutable fields: voice_id, language, model, instructions, speech_rate, caller_context. Empty patch is rejected with OVP_SESSION_UPDATE_REJECTED. Twilio supports only language swap.

Audio path metadata

Binary frames carry the bytes; these JSON events carry the metadata around them.

Transcripts

Agent reasoning + tool-use

OVP splits the LLM lifecycle into four distinct event types. Twilio collapses these into one text channel.

Interrupts — three distinct types

This is where Twilio’s interrupt falls short. OVP distinguishes:

Multi-modal sends

The agent can send media to the caller mid-call. Useful when the call is happening alongside SMS / WhatsApp / RCS (the customer’s phone has both channels available).

Realtime observability

These events are emitted continuously during the call. Pipe them to your dashboard, your billing UI, your QA tool — Orbit does not charge for them and they fire even when no one is subscribed (so reconnects can replay).

Human handoff lifecycle

DTMF

Error

recoverable: true means the WS stays open. recoverable: false is always followed by session.end.

Worked example: simple Q&A call

Caller dials a DID, asks one question, hangs up.

Worked example: tool call mid-conversation

Note: tool invocations are observable as their own events. A dashboard can render “agent called lookup_order with {order_id:12345}, returned in 412ms” without parsing the text channel.

Worked example: human handoff


Error codes


Compared to Twilio ConversationRelay


SDK

The SDK handles framing, sequence-number bookkeeping, automatic reconnect with resume, and direction validation. Full reference: SDK reference.