output_format="ulaw_8000"), which is the format
Twilio plays. The server forwards each audio chunk as it arrives, with no resampling or transcoding.
How it works
- A call reaches your Twilio number. Twilio requests your webhook and gets TwiML with
<Connect><Stream>. - Twilio opens a WebSocket to your server and sends
connected, thenstartwith thestreamSid. - Your server streams KugelAudio TTS with
output_format="ulaw_8000"and sends every chunk to Twilio as amediamessage (base64 µ-law, 8000 Hz, mono). - After each reply the server sends a
mark. Twilio echoes it when that audio has finished playing. - When the caller interrupts, the server stops the KugelAudio stream and sends
clear, which empties the audio Twilio has buffered.
1. Install
- A KugelAudio API key from the dashboard (Settings, API Keys). See Authentication.
- A voice ID. Pick one in Voices; the example uses
1071. - A Twilio phone number with voice capability.
- A public HTTPS host for your server. Twilio connects to the stream over
wss://only. For local testing, a tunnel such as ngrok works:ngrok http 8000.
2. The TwiML
Twilio needs this TwiML when a call comes in. The server below returns it fromPOST /twiml; you can
also paste it into a TwiML Bin. Replace YOUR_PUBLIC_HOST with your host name, without https://.
<Parameter> elements; they arrive in start.customParameters.
3. The server
Save this asserver.py. It greets the caller, answers each key press, and stops talking the moment a
key is pressed. Key presses stand in for barge-in here so the example runs without speech recognition.
server.py
reuse_connection=False gives every reply its own KugelAudio WebSocket. Cancelling a reply closes that
connection, so no audio of an interrupted reply reaches the next one.
4. Point your Twilio number at the server
In the Twilio Console, open your phone number’s voice configuration and set the webhook for incoming calls tohttps://YOUR_PUBLIC_HOST/twiml with HTTP POST. Call the number: you hear the greeting, and
pressing a key cuts it off and answers.
Adding speech recognition and an LLM
The example shows where the rest of a voice agent connects:- Caller audio. Each inbound
mediamessage carries base64 µ-law at 8000 Hz inmedia.payload. Send it to your speech recognition. - Replies. Call
call.say(text)with the LLM reply. For lower latency, call it once per complete sentence as the LLM streams; the sentences play in order. See Chunking and latency for where to split, and Voice prompting for prompts that read well aloud. - Barge-in. When your voice activity detection hears the caller speak over the agent, call
await call.interrupt()instead of reacting to a key press. See Barge-in. - End of turn.
on_markruns when a reply has finished playing on the call. Start an end-of-turn timer or open the microphone there.
Twilio messages used
Twilio’s WebSocket messages reference
has every field.
Troubleshooting
The call connects, then hangs up
The call connects, then hangs up
Twilio could not open the WebSocket. Check that the stream URL starts with
wss://, that
PUBLIC_HOST is the host only (no https://, no path), and that the tunnel forwards WebSocket
upgrades to port 8000.Noise or static instead of speech
Noise or static instead of speech
The audio is not µ-law at 8000 Hz. Keep
output_format="ulaw_8000" and do not send a WAV header
in media.payload. See Audio formats.The server logs an authentication error
The server logs an authentication error
Check
KUGELAUDIO_API_KEY. See Authentication and
Errors.Next steps
Audio formats
Every output format, including µ-law and a-law at 8000 Hz
Barge-in
Stopping speech when the caller interrupts
Voices
Find and pick a voice ID
Regions
Keep traffic in the EU