Skip to main content
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 beyond httpx, 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
  • ngrok or any HTTPS tunnel that supports WebSockets
  • Python 3.11+

Quick start

1

Install the packages

2

Set environment variables

Create .env:
3

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.
4

Run the server and expose it

Copy ngrok’s HTTPS URL into .env as PUBLIC_URL and restart the server.
5

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.
6

Place the call

Create outbound.py and run it with python outbound.py. Your phone rings; answer, talk, and the bot reads your words back.

Configuration

The Cartesia-facing knobs live in the generation request and the STT URL:
ParameterWhereValue usedNotes
model_idTTS requestsonic-3.5Pin a dated Sonic snapshot (e.g. sonic-3.5-2026-05-04) for production stability
voiceTTS request{"mode":"id","id":...}Any voice ID from the voice library
output_formatTTS requestpcm_mulaw @ 8000Matches Bandwidth’s audio/pcmu; no conversion needed
modelSTT URLink-2Cartesia’s latest streaming STT model
encoding / sample_rateSTT URLpcm_mulaw / 8000Matches the call’s native format

What’s next

  • Plug in an LLM. The echo in _transcripts_to_replies is the seam — route each finalized transcript through your own agent and synthesize its reply with the same speak call.
  • Cleaner turn-taking. The manual STT socket emits incremental is_final segments, 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 playAudio also accepts audio/pcm;rate=16000 and rate=24000 (mono, 16-bit, little-endian). Set Sonic’s output_format to pcm_s16le at 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-bandwidth wraps this protocol as a Pipecat FrameSerializer with 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="...">.

Resources