Run the complete example. Scaffold this guide’s full, CI-tested code in one
command, no clone:Or browse it:
twilio-omni-voice-agent.How it fits together
Twilio’s<Connect><Stream> opens a bidirectional WebSocket to your server:
it sends the caller’s audio as base64 G.711 μ-law at 8 kHz, and accepts the
agent’s audio back the same way. Omni speaks PCM16. Run Omni at 8 kHz
(?rate=8000) and the only conversion you do is μ-law companding, no
resampling, because both sides are already at 8 kHz.
This guide is correct on transport, the Twilio Media Streams message types, the
codec/rate math, and the Omni event behaviors. The exact JSON payloads of Omni
events (e.g. the
dtmf and transfer_to_human frames) are defined by the
Omni wire protocol, we isolate them in one place so
they’re trivial to update.Prerequisites
1
A Twilio number
A voice-capable phone number in your Twilio console, plus your Account SID and
Auth Token (for the transfer step).
2
A key
A
pyai_test_ key. Omni is zero-state, there’s no agent to create; set the
key as an environment variable on the server (never hard-code it). Optionally
pick a session_label to tag each call in your kb_endpoint.3
A public URL
Twilio must reach your server over TLS. For local dev, tunnel with
ngrok http 8080 and use the https/wss host it prints.Project layout
package.json
alawmulaw does the G.711 companding (mulaw.decode → Int16, mulaw.encode →
μ-law bytes); ws is the client socket to Omni; twilio is only used for the
REST transfer.
Build it
1
Serve TwiML that opens a bidirectional stream
When Twilio receives the call it fetches TwiML from your Set the number’s A call comes in webhook to
/voice route.
<Connect><Stream> (not <Start><Stream>) gives you a two-way socket so you
can send the agent’s audio back.server.js (TwiML route)
https://<PUBLIC_HOST>/voice (HTTP POST) in the Twilio console.2
Bridge the media WebSocket to Omni
Twilio connects to
/media and sends JSON frames: start (carries
streamSid + callSid), media (base64 μ-law), dtmf, and stop. For each
caller frame, decode μ-law → PCM16 and forward it to Omni as a 0x01-prefixed
binary frame. The tag is mandatory: an untagged frame is silently dropped and
the agent never hears the caller.server.js (caller → agent)
3
Relay agent audio back to the caller
Omni sends agent audio as binary PCM16 frames and session state as
text JSON. Encode PCM16 → μ-law, base64 it, and send a Twilio
media
message tagged with the streamSid.server.js (agent → caller)
4
Barge-in, DTMF, and transfer to a human
Three behaviors live in one event handler. Barge-in is the important one:
when the caller talks over the agent, Omni sends
flush. Twilio buffers
outbound audio, so you must tell it to drop what’s queued with a clear
message, otherwise the agent keeps talking over the caller.server.js (Omni events + helpers)
The inbound call-control event names (
flush, transfer_to_human,
send_dtmf, play_hold, collect, end_call, session_end) are stable
(server frames are keyed on event); your outbound configure / dtmf frames
are keyed on type. Omni emits these, but the carrier action only happens
because this handler performs it, so a call-control tool you enable in the
console does nothing until its case is wired here. Exact fields:
Omni wire protocol §4.1.Run it
https://<PUBLIC_HOST>/voice, then
call the number. You should hear the agent greet you within a second of the call
connecting. Talk over it to confirm barge-in cuts the agent off; press a key to
confirm DTMF flows through.
Codec & rate notes
- μ-law ↔ PCM16 only. Twilio is 8 kHz μ-law; running Omni at
rate=8000means you never resample,mulaw.decode/mulaw.encodeis the whole codec path. If you ever bridge an 8 kHz leg to a 16 kHz Omni session you’d upsample 2:1 (16000 / 8000 = 2); for Twilio, don’t, keep both at 8 kHz. - Frame size. Twilio sends ~20 ms (160 μ-law bytes) per
mediamessage. Relay agent audio in similar ~20 ms chunks for smooth playback; sending huge bursts can make Twilio’s jitter buffer stutter. - Tag every outbound
mediawith thestreamSidfrom thestartevent, or Twilio drops it silently.
Troubleshooting
Next steps
Omni wire protocol
Exact event payloads and close codes.
Browser voice agent
The same agent, in the browser with WebRTC.
FreeSWITCH integration
Fork SIP/PSTN audio into Omni at 16 kHz.
Errors & limits
Rate limits, concurrency, and retries.