
<Stream> and you have a phone-callable agent that a caller can interrupt mid-sentence — no separate STT vendor, no TTS vendor, and no turn logic of your own.
Source code: vobiz-ai/Vobiz-Deepgram-Voice-Agent — the reference FastAPI bridge used throughout this guide (app.py, call.py, mock_vobiz.py), plus a mock client that exercises the whole protocol without placing a call.
Scope: inbound and outbound. Attach a number to an application whose answer URL points at
/answer, or place an outbound call with the same URL — the bridge does not care which direction the call came from.How it works
wss:// URL is the server. Caller audio arrives as media events, the agent’s speech goes back as playAudio, and app.py is a thin relay: format handling, barge-in, and the Vobiz control protocol. Deepgram owns the conversation.
- Vobiz fetches
/answerand receives a<Stream>element. - Vobiz opens a WebSocket to
/media/<secret>and sends astartevent. - Caller audio arrives as
mediaevents and is forwarded to Deepgram. - Agent audio comes back as raw bytes and goes to Vobiz as
playAudioevents. - When the caller interrupts, Deepgram signals it and the app sends
clearAudio. - After each turn a
checkpointis sent; Vobiz repliesplayedStreamonce the caller has actually heard it.
The bidirectional Stream protocol
Every event and control message on the socket —
start, media, dtmf, playedStream, clearedAudio, stop — and what you send back.Audio profiles
The two directions of a Vobiz bidirectional stream are configured independently. Both profiles below are pure passthrough — the bridge never resamples.mulaw matches the PSTN leg exactly and is the right default. l16 trades bandwidth for a wider band — 16 kHz in, 24 kHz out — and Deepgram emits 24 kHz natively, so nothing is degraded on the way to Vobiz.
playAudio accepts L16 at 8/16/24 kHz and mu-law at 8 kHz. See audio formats for the full matrix.
Requirements
No separate LLM key is needed. Deepgram manages the OpenAI connection and bills it through your Deepgram account.
If your WebSocket endpoint is IP-restricted, allow inbound TCP 443 from the Vobiz media fleet. The RTP rule (UDP 5000–65535) does not cover it — see IP whitelisting.
Step 1: Configure the bridge
.env
Dependencies
requirements.txt
Step 2: Run it
Step 3: Place a call
- Inbound
- Outbound
Vobiz decides what to do with an inbound call by looking up the Voice Application attached to the number that was dialled. A number on its own is not enough — create the application first, then attach a number to it.1. Create a Voice ApplicationIn the console, go to Voice Applications → Create application. Set Primary answer URL to 
2. Attach a numberOpen the application and attach one of your DIDs under Attached Numbers → Attach number. Calls to that number now fetch XML from your answer URL.
Dial the attached number with a
https://<public>/answer with method POST. Optionally set the Hangup URL to https://<public>/hangup to receive the call-ended webhook.

0 or +91 prefix — 09XXXXXXXXX or +919XXXXXXXXX. You should hear the greeting.See Applications for the full reference.The answer XML
The WebSocket URL is the element’s text content, not a
url="" attribute.
Inside the bridge
The agent settings
Both ends of the pipeline are Flux, Deepgram’s conversational speech models, and both live on v2 endpoints — so each provider must pinversion: "v2". Omit it and the provider falls back to v1, where the Flux model names are not valid.
think provider — for example {"type": "anthropic", "model": "claude-sonnet-5"}.
Barge-in
Deepgram detects the caller talking over the agent and emitsUserStartedSpeaking. Vobiz may still have seconds of the agent’s reply buffered, so the bridge flushes it:
clearedAudio event. Without this the agent keeps talking over the caller. See clearAudio.
Knowing the caller actually heard it
playAudio means sent, not heard. After each turn the bridge sends a checkpoint and Vobiz answers playedStream once the buffered audio has played out:
Frame slicing
Deepgram hands over arbitrarily sized audio chunks. Vobiz is happiest with steady telephony-sized frames, soVobizStream.play() re-slices into 20 ms — 160 bytes mu-law at 8 kHz, 960 bytes L16 at 24 kHz — rather than forwarding blindly.
Format mismatches are silent
Since the bridge never resamples, acontentType that disagrees with AUDIO_MODE is just garbage audio into the agent. read_start() compares the XML against start.mediaFormat and prints [audio] WARNING instead of failing quietly.
Security
Both public endpoints are reachable by anyone who learns the URL, so the bridge ships two opt-in controls:extraHeaders cannot authenticate the media socket. The values never reach the WebSocket — not as an upgrade header, and not in the start frame, whose extra_headers field stays the literal "{}". They surface only in the statusCallbackUrl payload, as X-VH-<key>. That makes extraHeaders status-callback metadata rather than stream credentials, which is why the secret rides in the URL path instead.The webhook signature covers the URL and a nonce, never the body. Voice webhooks are form-encoded, so any scheme that hashes a JSON body will not verify. Query parameters are stripped first, and behind a tunnel the public URL has to be rebuilt —
request.url is the internal address Vobiz never saw. Signature headers are only emitted when the callback URL has auth credentials configured on it, which is why VERIFY_SIGNATURE is opt-in. See Validating callbacks.Test without placing a call
mock_vobiz.py stands in for Vobiz — it speaks the media-stream protocol against a running app.py, answers checkpoint with playedStream, and reports what came back.
playAudio frames came back in the format the XML asked for.
Configuration reference
Troubleshooting
Next steps
- Clone the reference bridge: vobiz-ai/Vobiz-Deepgram-Voice-Agent
- Read the
<Stream>reference, audio formats, stream events andplayAudio - Deepgram’s Voice Agent API and Flux docs
- Bridging a different model over the same socket? See WebSockets and Gemini Live