Telephony with Twilio
Answer phone calls over a media stream.
The Twilio plugin turns an inbound phone call into an AgentSession bound to the
call's audio. Twilio connects a Media Stream (a WebSocket carrying μ-law
audio) to your server; each call becomes one session over the generic WebSocket
transport.
The shape
Two endpoints:
- Voice webhook (
POST) — Twilio hits this when a call comes in; you answer with TwiML telling Twilio to open a bidirectional media stream. - Media WebSocket (
GET) — each call's audio; bound to oneAgentSession.
Serve the webhook
The plugin builds the TwiML and provides an HTTP handler:
import "github.com/webdeveloperben/agents-go/plugins/twilio"
http.Handle("/twilio/voice", twilio.WebhookHandler(twilio.WebhookConfig{
// StreamURL: wss://<host>/twilio/media (derived from the request if empty)
}))twilio.BuildTwiML(streamURL, params) is available if you build the response
yourself.
Bind media to a session
Each media WebSocket becomes one AgentSession. Write the per-call logic once as
a websocket.SessionFunc (func(ctx, *websocket.Transport) error): build the
session bound to t.Input() / t.Output(), start it, then block on t.Wait(ctx)
until the caller hangs up.
func runSession(ctx context.Context, t *websocket.Transport) error {
session, err := agents.NewSession(agents.Config{
LLM: "openai/gpt-4o", STT: "openai/gpt-4o-mini-transcribe", TTS: "openai/tts-1",
Input: agents.SessionInput{Audio: t.Input()}, // Twilio media in
Output: agents.SessionOutput{Audio: t.Output()}, // agent audio out
})
if err != nil {
return err
}
defer func() { _ = session.Close(ctx) }()
agent, err := agents.NewAgent(agents.AgentOptions{Instructions: "You answer the phone."})
if err != nil {
return err
}
if err := session.Start(ctx, agents.StartOptions{Agent: agent}); err != nil {
return err
}
t.Wait(ctx) // block until the caller hangs up or the socket closes
return nil
}No sample-rate wiring needed: TTS output is resampled to Twilio's 8 kHz wire
automatically, and an STT that declares a fixed input rate
(STTCapabilities.SampleRate) has inbound audio
resampled to it — so you never hand-match rates.
Single process (start here)
websocket.Handler runs your SessionFunc directly — one process, no worker or
dispatcher. A single process handles many concurrent calls.
mux.Handle("/twilio/media", websocket.Handler(twilio.New(), runSession))The runnable examples/twilio-single-process wires both endpoints this way.
Worker fleet (horizontal scale)
When one process can no longer hold your call volume, run the same
SessionFunc inside a worker and route calls
through a dispatcher. websocket.NewSessionHandoff bridges the accepted socket to
the worker's entrypoint, and websocket.DispatchHandler accepts the socket and
dispatches a job:
handoff := websocket.NewSessionHandoff()
w, err := worker.New(worker.WorkerOptions{
Entrypoint: handoff.Entrypoint(runSession), // the same SessionFunc
})
if err != nil {
log.Fatal(err)
}
// ... w.Run(ctx, dispatchSource) in the background ...
mux.Handle("/twilio/media", websocket.DispatchHandler(twilio.New(), dispatcher, jobs, handoff))The runnable examples/twiliovoice wires the worker path end to end with the
in-repo fakes. See Scaling & load for when and
how to grow into it.
Which one? Start with websocket.Handler. Move to the worker + dispatcher
path only when you need to scale across processes or machines, add load-aware
placement, or drain gracefully on deploy. The
SessionFunc is identical either way, so switching is a wiring change, not a
rewrite.
Run it
Expose your server
Twilio needs a public HTTPS URL. In development, tunnel it — e.g. ngrok http 8080.
Point your number at the webhook
Set the Twilio number's Voice webhook to https://<tunnel>/twilio/voice.
Call the number
The webhook answers with TwiML, Twilio opens the media stream, and your agent picks up.
The session code is identical to any other transport — only the I/O binding differs. Swapping the fakes for real provider plugins doesn't touch the Twilio wiring.
Related
- Plugin: Twilio — webhook config and framing details.
- Transports · Architecture
- Deploy a worker