Skip to main content
Deepgram The Deepgram Voice Agent API runs speech-to-text, the LLM and text-to-speech behind a single WebSocket, with its own turn-taking. Bridge it to a Vobiz <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

Vertical call flow: caller audio travels from the caller over PSTN to Vobiz, over a bidirectional WebSocket as media events to app.py, and into the Deepgram Voice Agent as send_media. Agent audio returns as audio bytes, then playAudio, then PSTN. A dashed barge-in loop runs from Deepgram back to Vobiz, labelled UserStartedSpeaking then clearAudio.Vertical call flow: caller audio travels from the caller over PSTN to Vobiz, over a bidirectional WebSocket as media events to app.py, and into the Deepgram Voice Agent as send_media. Agent audio returns as audio bytes, then playAudio, then PSTN. A dashed barge-in loop runs from Deepgram back to Vobiz, labelled UserStartedSpeaking then clearAudio.
Vobiz opens the WebSocket to you — your 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.
  1. Vobiz fetches /answer and receives a <Stream> element.
  2. Vobiz opens a WebSocket to /media/<secret> and sends a start event.
  3. Caller audio arrives as media events and is forwarded to Deepgram.
  4. Agent audio comes back as raw bytes and goes to Vobiz as playAudio events.
  5. When the caller interrupts, Deepgram signals it and the app sends clearAudio.
  6. After each turn a checkpoint is sent; Vobiz replies playedStream once 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.
Never put rate=24000 on <Stream contentType>. That attribute configures the inbound direction, which tops out at 16 kHz. 24 kHz is outbound-only — valid on playAudio and nowhere else.
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

Expose the server publicly, then confirm what it resolved:
Whichever host you use becomes the answer URL below.

Step 3: Place a call

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 https://<public>/answer with method POST. Optionally set the Hangup URL to https://<public>/hangup to receive the call-ended webhook.
Vobiz console Create application dialog with fields for application name, primary answer URL with a POST method selector, hangup URL, and fallback answer URL
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.
Attached Numbers panel on a Vobiz voice application showing no numbers attached yet and an Attach number button
Dial the attached number with a 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.
Malformed answer XML is not an HTTP error. Vobiz accepts the 200, then drops the call about a second later, and the only trace is the CDR field hangup_cause_name: "Invalid Answer XML". A URL carrying two query parameters contains a bare &, which is enough on its own to invalidate the document — the reference bridge runs every interpolated value through html.escape() for this reason.

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 pin version: "v2". Omit it and the provider falls back to v1, where the Flux model names are not valid.
Sending the settings frame is what starts the conversation, greeting included. To swap the LLM, change the think provider — for example {"type": "anthropic", "model": "claude-sonnet-5"}.

Barge-in

Deepgram detects the caller talking over the agent and emits UserStartedSpeaking. Vobiz may still have seconds of the agent’s reply buffered, so the bridge flushes it:
Vobiz confirms with a 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:
That signal is what lets an agent say goodbye and then hang up without clipping its own last word.

Frame slicing

Deepgram hands over arbitrarily sized audio chunks. Vobiz is happiest with steady telephony-sized frames, so VobizStream.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, a contentType 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.
A pass means playAudio frames came back in the format the XML asked for.

Configuration reference

Troubleshooting

Next steps