Telephony: Bandwidth
Bandwidth is a Tier 1 carrier and CPaaS provider. Its Programmable Voice API can stream a live call’s audio to your server over a WebSocket. This guide builds a small FastAPI bridge that takes that audio, transcribes it with Pulse, writes a reply with Electron, and speaks it back with Lightning v3.1, so callers on a Bandwidth number talk to a voice agent you control end to end.
Bandwidth delivers call audio as 8 kHz G.711 mu-law. Pulse accepts mu-law at 8 kHz and Lightning can return mu-law at 8 kHz, so audio passes straight through the bridge with no resampling or transcoding.
This guide uses Programmable Voice with media streaming, so you write and host the bridge yourself and choose every model and prompt. If you would rather have Smallest AI host the agent and connect your Bandwidth numbers over SIP, see SIP Trunking.
How it works
- A caller dials your Bandwidth number. Bandwidth sends a call initiated webhook to
/bxml, authenticated with HTTP Basic Auth. - The bridge replies with BXML:
<StartStream mode="bidirectional">opens a two-way media WebSocket, and<StopStream wait="true"/>keeps the call up for as long as the stream runs. - Bandwidth connects to
/stream/{token}. The token is single-use and expires after 30 seconds, so the bridge only trusts call IDs that came from the authenticated webhook, never from the unauthenticated WebSocketstartevent. - Caller audio is forwarded to the Pulse STT WebSocket as raw mu-law bytes. With
vad_events=true, Pulse reports when the caller starts and stops speaking. - When the caller stops speaking, the bridge sends
finalize, collects the final transcript, asks Electron for a reply, and streams it through Lightning TTS back to Bandwidth asplayAudioevents. - If the caller talks over the agent, the bridge cancels the reply and sends Bandwidth a
clearevent, which drops any audio still queued for playback (barge-in).
Audio formats
Declare the rate you actually send. Pulse does not detect the sample rate. If you send 8 kHz audio but declare sample_rate=16000, the transcript comes back wrong with no error.
Prerequisites
- Python 3.10+
- A Smallest AI API key from the console
- A Bandwidth account with Programmable Voice, at least one phone number, and access to the Bandwidth App
- A public HTTPS host for the bridge. For local testing, ngrok works:
ngrok http 8000
Setup
1. Configure Bandwidth
In the Bandwidth App:
- Create a Voice Application. Under Call initiated, choose
POSTand set the callback URL tohttps://<your-public-host>/bxml. - Tick Use a callback username and password and enter a username and password. You will put the same values in
.env. - Link the application to a Voice Configuration Package, and assign your phone number to that package.
On the Bandwidth Build self-serve tier, a Voice Application and Voice Configuration Package are created when you activate your account. Update the callback URL and credentials on the existing application instead of creating a new one.
2. Create the project
Create a .env file:
3. Add the bridge
Save this as server.py:
4. Run it
Call your Bandwidth number. The agent greets you, and you can talk to it. Interrupt it mid-sentence and it stops and listens.
Outbound calls
To have the agent place a call, create the call with the Bandwidth Voice API and point answerUrl at the same /bxml endpoint. When the person answers, Bandwidth requests BXML and the call joins the same bridge.
Bandwidth API calls use OAuth 2.0 client credentials. Create an API credential (client ID and secret) in the Bandwidth App, then:
The username and password fields make Bandwidth send Basic Auth on the answer webhook, so /bxml accepts it.
Going to production
- Stream the reply. The example waits for Electron’s full answer before speaking. Use streaming chat completions and send sentences to Lightning as they arrive, with a shared
context_idso prosody carries across fragments (Continuations). - Reuse the TTS connection. Open one Lightning WebSocket per call instead of one per reply to save a connection handshake on every turn.
- Pick your languages. Set
SMALLEST_LANGUAGEto a code Pulse and your chosen voice both support. For English-only agents, Pulse 2.0 adds built-in end-of-turn detection. - Run close to your callers. Lightning and Pulse serve from India and US West. Host the bridge in the same region as your inference pool to keep round trips short.
- Store tokens outside memory. The in-memory token map works for a single process. Use Redis or similar if you run several bridge replicas.
- Hang up cleanly. To end a call from the agent, send
{"state": "completed"}toPOST https://voice.bandwidth.com/api/v2/accounts/{accountId}/calls/{callId}with your Bearer token. TheaccountIdandcallIdare stored on eachCallfrom the webhook.