Last verified: 2026-06-18
Overview
Bridge a live phone call on Bandwidth Programmable Voice to Cartesia: transcribe the caller with Ink 2 speech-to-text and reply with Sonic text-to-speech. A FastAPI server returns BXML, accepts Bandwidth’s bidirectional media WebSocket, and forwards audio to and from Cartesia’s STT and TTS sockets — no SDKs beyondhttpx, fastapi, uvicorn, and websockets.
Bandwidth carries calls as 8 kHz μ-law, and both Cartesia sockets speak pcm_mulaw at 8000 Hz, so audio crosses the bridge byte-for-byte — no resampling in either direction.
Prerequisites
- A Cartesia API key (looks like
sk_car_...) and a voice ID from the voice library - A Bandwidth account with Voice API OAuth credentials (
client_id/client_secret), a phone number, and a Voice Application. Bandwidth Build is a free self-serve tier — sign up for trial credits and a US number, no card required ngrokor any HTTPS tunnel that supports WebSockets- Python 3.11+
Quick start
Write the bridge server
Create
server.py. It answers Bandwidth’s webhook with a <StartStream> verb, opens the media WebSocket, forwards caller audio to Ink 2, and speaks each finalized transcript back through Sonic.Point the Voice Application at the bridge
In the Bandwidth dashboard, edit your Voice Application and set its Inbound Voice URL to
https://your-subdomain.ngrok.app/bxml. The Voice Application must be valid even for outbound calls; the URL used per call is set as answerUrl in the next step.Configuration
The Cartesia-facing knobs live in the generation request and the STT URL:| Parameter | Where | Value used | Notes |
|---|---|---|---|
model_id | TTS request | sonic-3.5 | Pin a dated Sonic snapshot (e.g. sonic-3.5-2026-05-04) for production stability |
voice | TTS request | {"mode":"id","id":...} | Any voice ID from the voice library |
output_format | TTS request | pcm_mulaw @ 8000 | Matches Bandwidth’s audio/pcmu; no conversion needed |
model | STT URL | ink-2 | Cartesia’s latest streaming STT model |
encoding / sample_rate | STT URL | pcm_mulaw / 8000 | Matches the call’s native format |
What’s next
- Plug in an LLM. The echo in
_transcripts_to_repliesis the seam — route each finalized transcript through your own agent and synthesize its reply with the samespeakcall. - Cleaner turn-taking. The manual STT socket emits incremental
is_finalsegments, so this demo replies per fragment. Switch to the turn-detection endpoint (/stt/turns/websocket) to reply once per completed utterance. - Higher-fidelity TTS. Bandwidth’s
playAudioalso acceptsaudio/pcm;rate=16000andrate=24000(mono, 16-bit, little-endian). Set Sonic’soutput_formattopcm_s16leat the matching rate; Bandwidth resamples to 8 kHz μ-law once at its edge instead of after a lossy round-trip. - Barge-in. Send
{"eventType": "clear"}on the media WebSocket to drop queued outbound audio when the caller talks over the bot. - Use a framework.
pipecat-bandwidthwraps this protocol as a PipecatFrameSerializerwith the STT/LLM/TTS plumbing built in. - Harden it. Validate Bandwidth’s webhook signatures and attach Basic auth to the WebSocket via
<StartStream destinationUsername="..." destinationPassword="...">.