Build an AI phone agent that handles real calls using Twilio Voice + Media Streams and the AssemblyAI Universal-3.6 Pro Realtime model for real-time speech-to-text.
The key detail: Twilio streams 8 kHz μ-law (mulaw) audio, and Universal-3.6 Pro Realtime accepts pcm_mulaw at sample_rate=8000 natively — no resampling, no format conversion.
Architecture
Incoming call
│
Twilio Voice
│ TwiML → open WebSocket
▼
Your server (/media-stream WebSocket)
│ │
│ mulaw 8kHz audio │ synthesized mulaw audio
▼ ▲
AssemblyAI Universal-3.6 ElevenLabs TTS
Pro Realtime
│ transcript + turn signal
▼
OpenAI GPT-4oPrerequisites
- Python 3.11+
- AssemblyAI API key (free account)
- Twilio account with a phone number
- OpenAI API key
- ElevenLabs API key
- ngrok (for local development)
Quick start
git clone https://github.com/kelsey-aai/voice-agent-twilio-universal-3-5-pro
cd voice-agent-twilio-universal-3-5-pro
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env
# Edit .env with your API keys
uvicorn server:app --host 0.0.0.0 --port 8000
ngrok http 8000
Configure Twilio
- Go to Twilio Console → Phone Numbers
- Select your number → Voice & Fax
- Set A Call Comes In to Webhook: https://your-ngrok-url.ngrok.io/incoming-call
- Call your Twilio number
AssemblyAI WebSocket parameters for Twilio
ASSEMBLYAI_WS_URL = (
"wss://streaming.assemblyai.com/v3/ws"
"?speech_model=universal-3-6-pro"
"&encoding=pcm_mulaw" # must match Twilio's audio format
"&sample_rate=8000" # must match Twilio's 8kHz stream
"&min_turn_silence=400" # phone audio: wait a beat longer before ending the turn
"&max_turn_silence=2000" # hard ceiling so deliberate callers aren't cut off
)
Phone calls carry more background noise than browser audio, so a slightly longer min_turn_silence reduces premature turn endings. Universal-3.6 Pro Realtime uses end-of-turn detection that combines semantic context with voice activity, and entity-aware endpointing holds the turn open while a caller reads out a phone number, code, or email. You can also set the high-level mode preset (min_latency, balanced, max_accuracy) to shift the whole accuracy/latency balance at once.
Note: end_of_turn_confidence_threshold does not apply to Universal-3.6 Pro Realtime — that parameter belongs to the older universal-streaming models. Use min_turn_silence / max_turn_silence here.Heads up on model IDs: this tutorial uses universal-3-6-pro, the current streaming default. universal-3-5-pro stays available if you need to pin the previous model; if you're still on the legacy u3-rt-pro ID, switch to universal-3-6-pro.
Stream Twilio's 8 kHz audio straight into Universal-3.6 Pro Realtime — no transcoding, accurate transcription of account numbers and emails, turn detection built for phone calls.
Extending the agent
Sharpen accuracy with conversation context
Universal-3.6 Pro Realtime can transcribe each caller turn in the context of what your agent just said — after your agent asks *"What's your account number?"*, the model is primed for the answer. On a raw WebSocket, push your agent's last reply mid-stream with an UpdateConfiguration message after each agent turn:
await aai_ws.send(json.dumps({
"type": "UpdateConfiguration",
"agent_context": "Thanks for calling Acme. What's the account number on the
policy?",
}))
Handle noisy lines with Voice Focus
For calls from cars, speakerphones, or noisy rooms, add Voice Focus to isolate the caller's voice server-side:
ASSEMBLYAI_WS_URL += "&voice_focus=far-field"
Add keyterm prompting
ASSEMBLYAI_WS_URL += "&keyterms_prompt=YourBrand&keyterms_prompt=SpecialTerm"
Add post-call transcription
import assemblyai as aai
transcriber = aai.Transcriber()
transcript = transcriber.transcribe(recording_url)
print(transcript.text)
Test Universal-3.6 Pro Realtime on your own recordings in the Playground — hear how its turn detection and Voice Focus handle noisy calls.
Deploy to Railway or Render
# Railway
railway login && railway init && railway up
# Render — create a Web Service pointing to this repo
# Start: uvicorn server:app --host 0.0.0.0 --port $PORT
Resources
- Universal-3.6 Pro Realtime streaming API reference
- AssemblyAI streaming getting started
- Twilio Media Streams docs
Start free with Universal-3.6 Pro Realtime — $0.45/hr, keyterm prompting included, no minimums. Point Twilio at your server and go live in about half an hour.
Frequently asked questions
What is the best speech-to-text model for a Twilio phone agent?
AssemblyAI's Universal-3.6 Pro Realtime is well suited to Twilio phone agents because it accepts Twilio's native 8 kHz pcm_mulaw stream directly — no resampling or transcoding — and posts a 5.19% word error rate on AssemblyAI's English voice-agent benchmark, with a 0.96% pooled semantic WER on Pipecat's open benchmark. Its turn detection, which combines semantic context with voice activity, and Context Carryover are built for the back-and-forth of phone conversations, and it handles the account numbers, phone numbers, and spelled-out entities that come up constantly on support calls.
How do I connect Twilio Media Streams to AssemblyAI?
Point your Twilio number's incoming-call webhook at a server that returns TwiML opening a Media Stream to your WebSocket endpoint. In that endpoint, open a WebSocket to wss://streaming.assemblyai.com/v3/ws?speech_model=universal-3-6-pro&encoding=pcm_mulaw&sample_rate=8000, forward Twilio's base64 media payloads as audio, and act on AssemblyAI's Turn messages. Because both sides speak 8 kHz μ-law, no format conversion is needed.
Do I need to convert Twilio's audio format for AssemblyAI?
No. Twilio streams 8 kHz μ-law and Universal-3.6 Pro Realtime accepts encoding=pcm_mulaw at sample_rate=8000 natively, so you can pass Twilio's audio straight through. This removes a whole class of latency and quality bugs that come from resampling on the fly.
How do I reduce false turn endings on noisy phone calls?
Raise min_turn_silence (e.g. to 400 ms) so brief line noise doesn't trigger an early turn end, keep a max_turn_silence ceiling (e.g. 2000 ms) so deliberate callers aren't cut off, and enable voice_focus=far-field to suppress background noise server-side. For a one-line adjustment, set mode="max_accuracy" to bias the whole pipeline toward cleaner transcripts.
How much does a Twilio + AssemblyAI phone agent cost?
AssemblyAI's Universal-3.6 Pro Realtime is $0.45/hr for speech-to-text, billed on session duration with keyterm prompting included. Twilio Voice, your LLM (e.g. OpenAI), and your TTS provider (e.g. ElevenLabs) bill separately through their own accounts. AssemblyAI offers a free tier to build and test before going live.