Skip to main content
Gemini Live does speech-to-text, the LLM and text-to-speech inside a single WebSocket session, with its own voice activity detection. Bridge it to a Vobiz <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

Vobiz opens the WebSocket to you — your 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.
Never put rate=24000 on <Stream>. Inbound L16 at 24 kHz is accepted by the XML parser and the platform attempts the stream, but your application never receives usable media. 24 kHz is valid only on playAudio.This is exactly why the working combination for a 24 kHz TTS engine is 16 kHz in, 24 kHz out.
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 close 1008 and no useful message. Check first:
It lists every model your key can use with bidiGenerateContent. Two worth knowing:

Step 2: Configure the bridge

.env
Pin GEMINI_LANGUAGE for a real phone line. With language detection on auto, background noise and accented speech get transcribed as whatever language fits best, and the model answers in kind. On an Indian line, en-IN keeps it steady.

Dependencies

requirements.txt

Step 3: Run it

app.py prints the URLs it is serving:

Step 4: Place a call

It discovers the answer URL from the running server and posts to Make a Call:
While a call is up, or after it:
Each call is also written to data/call-<uuid>.json when it ends.

The answer XML

Malformed answer XML is not an HTTP error. Vobiz accepts the 200, then drops the call about a second later. The only trace is the CDR field hangup_cause_name: "Invalid Answer XML" with hangup_source: Error.A URL carrying two query parameters contains a bare &, which is by itself enough to invalidate the document — escape it. The reference bridge runs every interpolated value through html.escape() for this reason.
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 reports server_content.interrupted the moment the caller talks over the agent. Forward it to Vobiz as clearAudio:
That drops the playback still queued on the call leg. Queued audio can be seconds long, so skip this and the caller keeps hearing a reply the model has already abandoned.

End of turn, and hanging up cleanly

After each turn the bridge sends a checkpoint. Vobiz answers playedStream once the queued audio has actually played — the only reliable signal that the caller heard something:
That is what lets the 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 as dtmf 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

Decode according to 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 per playAudio. 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.
The prompt is spoken by Gemini TTS and cached in media/, so no recording and no second provider is needed.

Verified behaviour

Run against gemini-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. Set PUBLIC_URL and ngrok is skipped:
  • Behind a reverse proxy, WebSocket upgrades on /ws must 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.
Transcripts and recordings are personal data in most jurisdictions. The bridge writes data/call-<uuid>.json per call and media/reply.wav on mock runs, and .env holds your Gemini key and Vobiz auth token. All three are gitignored in the reference repo. If you add storage, decide on retention before you add the feature, and tell callers.

24 kHz playback and the handset

24 kHz on playAudio 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