<Stream> and you have a phone-callable voice agent with no separate STT vendor, no TTS vendor, and no turn-taking logic of your own.
Source code: vobiz-ai/Vobiz-Gemini-Live-Streaming — the reference FastAPI bridge used throughout this guide (app.py, gemini_live.py, audio.py), plus a mock client that holds a whole conversation without placing a call.
Scope: inbound and outbound. Point a Vobiz number’s answer URL 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, you send the model’s speech back as playAudio, and what is left for your code to do is format handling, barge-in, and the Vobiz control protocol.
The bidirectional Stream protocol
Every event and control message on the socket —
start, media, dtmf, playedStream, clearedAudio, stop — and what you send back.Why the default formats matter
The two directions of a Vobiz bidirectional stream are independent, and Gemini Live is fixed at 16 kHz in / 24 kHz out. Line them up and no audio is resampled anywhere in the path:audio/x-mulaw;rate=8000 and audio/x-l16;rate=8000 also work — the bridge’s audio.py converts — but each conversion costs a little quality and latency.
See Audio formats for the full matrix of supported rates and encodings.
Requirements
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: Pick a Live model
Live API model names change often, and choosing one your key cannot use fails at connect time with a bare WebSocket close1008 and no useful message. Check first:
bidiGenerateContent. Two worth knowing:
Step 2: Configure the bridge
.env
Dependencies
requirements.txt
Step 3: Run it
app.py prints the URLs it is serving:
Step 4: Place a call
- Outbound
- Inbound
data/call-<uuid>.json when it ends.
The answer XML
If the session must be recorded,
<Record> and <Stream> are siblings — never nest them. Watch the Record action URL: if it answers <Hangup/>, the call ends before <Stream> ever runs.
Inside the bridge
Making the model speak first
The caller should never have to open the conversation. A client-content turn at session start makes the model greet them:Barge-in
Gemini’s VAD reportsserver_content.interrupted the moment the caller talks over the agent. Forward it to Vobiz as clearAudio:
End of turn, and hanging up cleanly
After each turn the bridge sends acheckpoint. Vobiz answers playedStream once the queued audio has actually played — the only reliable signal that the caller heard something:
end_call tool say goodbye and then hang up, instead of cutting the caller off mid-word: the tool call only sets a flag, and the socket closes when playedStream comes back.
A checkpoint discarded by
clearAudio may never complete. Do not block on one.DTMF
Keypad presses arrive asdtmf events. Forwarding them to the model as text is worth doing — a caller asked for a number will often type it:
Reading the format from the wire
start.mediaFormat on every connection rather than trusting your own configuration. Keep the streamId — every control message needs it.
Frame sizes
Send 20–60 ms perplayAudio. 20 ms gives responsive barge-in and predictable queueing.
L16 means signed 16-bit, mono, little-endian, raw — no RIFF/WAV header — base64-encoded.
Test without spending a phone call
mock_vobiz.py impersonates Vobiz: it opens the WebSocket, sends a real start frame, streams a spoken prompt, and writes the agent’s reply to media/reply.wav. It exits non-zero if no audio comes back, so it works in CI.
media/, so no recording and no second provider is needed.
Verified behaviour
Run againstgemini-3.1-flash-live-preview.
On a real PSTN call — 44 s answered, 2189 media frames in, 928 KB played:
Behaviour reference
Useful CDR fields
total_cost, stream_cdrs[].billed_amount, billsec, mos, jitter, packet_loss, codec, ring_time, hangup_disposition, hangup_cause_name.
Two timing traps: the hangup webhook’s
Duration counts answered seconds, while the CDR’s duration is wall clock including ringing — they disagree on every call. And billing rounds up to a 60-second minimum pulse, so a 44-second stream reports rounded_bill_duration: 60.Configuration reference
Deploying
Any host with a public HTTPS URL and WebSocket support works. SetPUBLIC_URL and ngrok is skipped:
- Behind a reverse proxy, WebSocket upgrades on
/wsmust be forwarded, and the idle timeout has to exceed your longest call. - ngrok issues a new domain on each restart, so an answer URL stored on a number or application must be updated whenever it changes.
24 kHz playback and the handset
24 kHz onplayAudio does not mean 24 kHz at the caller’s ear. If the phone leg negotiated G.711 (codec: PCMU), Vobiz downconverts anyway. The gain is in not degrading the TTS before it reaches Vobiz.
Next steps
- Clone the reference bridge: vobiz-ai/Vobiz-Gemini-Live-Streaming
- Read the
<Stream>reference, audio formats, stream events andplayAudio - Google’s Gemini Live API docs and the
google-genaiSDK - Bridging to something else over the same socket? See WebSockets