Twilio¶
Twilio provides programmable voice calls and SMS messaging.
Features¶
- Voice Calls: Inbound and outbound PSTN calls
- SMS: Send and receive text messages
- Media Streams: Real-time audio via WebSocket
- TwiML: Declarative call control
- Global Coverage: 180+ countries
Configuration¶
import (
"github.com/plexusone/omnivoice"
_ "github.com/plexusone/omnivoice/providers/twilio"
)
provider, err := omnivoice.GetCallSystemProvider("twilio",
omnivoice.WithAccountSID(os.Getenv("TWILIO_ACCOUNT_SID")),
omnivoice.WithAuthToken(os.Getenv("TWILIO_AUTH_TOKEN")),
omnivoice.WithPhoneNumber("+15551234567"), // Your Twilio number
omnivoice.WithWebhookURL("https://your-server.com/webhook"),
)
Voice Calls¶
Making Outbound Calls¶
call, err := provider.MakeCall(ctx, "+15559876543")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Call SID: %s\n", call.ID())
fmt.Printf("Status: %s\n", call.Status())
With Custom From Number¶
call, err := provider.MakeCall(ctx, "+15559876543",
omnivoice.WithFrom("+15551234567"),
omnivoice.WithTimeout(30 * time.Second),
)
Handling Incoming Calls¶
Set up webhook handler:
provider.OnIncomingCall(func(call omnivoice.Call) error {
log.Printf("Incoming call from %s", call.From())
// Answer the call
if err := call.Answer(ctx); err != nil {
return err
}
// Handle call logic...
return nil
})
// HTTP handler for webhook
http.HandleFunc("/webhook", func(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
callSid := r.Form.Get("CallSid")
from := r.Form.Get("From")
to := r.Form.Get("To")
// Return TwiML response
w.Header().Set("Content-Type", "application/xml")
w.Write([]byte(`<?xml version="1.0"?>
<Response>
<Say>Welcome to our service.</Say>
<Connect>
<Stream url="wss://your-server.com/media-stream" />
</Connect>
</Response>`))
})
Call Control¶
// Answer
err := call.Answer(ctx)
// Hang up
err = call.Hangup(ctx)
// Get status
status := call.Status() // ringing, answered, ended
// Get call info
fmt.Printf("Direction: %s\n", call.Direction()) // inbound, outbound
fmt.Printf("From: %s\n", call.From())
fmt.Printf("To: %s\n", call.To())
fmt.Printf("Duration: %s\n", call.Duration())
Media Streams¶
Connect real-time audio for voice agents:
TwiML Setup¶
<?xml version="1.0"?>
<Response>
<Connect>
<Stream url="wss://your-server.com/media-stream">
<Parameter name="customParam" value="value" />
</Stream>
</Connect>
</Response>
WebSocket Handler¶
transport := provider.Transport()
// Listen for connections
connCh, _ := transport.Listen(ctx, "/media-stream")
http.HandleFunc("/media-stream", func(w http.ResponseWriter, r *http.Request) {
transport.HandleWebSocket(w, r, "/media-stream")
})
// Handle audio streams
for conn := range connCh {
go handleMediaStream(conn)
}
func handleMediaStream(conn transport.Connection) {
defer conn.Close()
buffer := make([]byte, 1024)
for {
n, err := conn.AudioOut().Read(buffer) // Audio from caller
if err != nil {
break
}
// Process audio (send to STT)
processAudio(buffer[:n])
// Send response audio (from TTS)
conn.AudioIn().Write(responseAudio)
}
}
Audio Format¶
Twilio Media Streams use:
- Encoding: mulaw (μ-law)
- Sample Rate: 8000 Hz
- Channels: Mono
// Configure TTS for Twilio-compatible output
ttsConfig := omnivoice.SynthesisConfig{
VoiceID: "aura-asteria-en",
OutputFormat: "mulaw_8000", // Twilio-compatible
}
SMS Messaging¶
Sending SMS¶
// Cast to SMSProvider
sms := provider.(omnivoice.SMSProvider)
// Send using default number
msg, err := sms.SendSMS(ctx, "+15559876543", "Hello from OmniVoice!")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Message SID: %s\n", msg.ID)
fmt.Printf("Status: %s\n", msg.Status)
From Specific Number¶
Receiving SMS¶
http.HandleFunc("/sms-webhook", func(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
from := r.Form.Get("From")
body := r.Form.Get("Body")
log.Printf("SMS from %s: %s", from, body)
// Reply with TwiML
w.Header().Set("Content-Type", "application/xml")
w.Write([]byte(`<?xml version="1.0"?>
<Response>
<Message>Thanks for your message!</Message>
</Response>`))
})
TwiML Examples¶
Greeting with Speech Input¶
<?xml version="1.0"?>
<Response>
<Say voice="Polly.Joanna">Welcome. How can I help you?</Say>
<Gather input="speech" action="/handle-speech" timeout="3">
<Say>Please speak after the beep.</Say>
</Gather>
</Response>
Transfer Call¶
<?xml version="1.0"?>
<Response>
<Say>Transferring you now.</Say>
<Dial>+15559876543</Dial>
</Response>
Play Audio¶
Webhook Security¶
Validate Twilio webhook signatures:
import "github.com/twilio/twilio-go/client"
func validateWebhook(r *http.Request, authToken string) bool {
validator := client.NewRequestValidator(authToken)
signature := r.Header.Get("X-Twilio-Signature")
url := "https://your-server.com" + r.URL.Path
return validator.Validate(url, r.Form, signature)
}
Error Handling¶
call, err := provider.MakeCall(ctx, to)
if err != nil {
switch {
case strings.Contains(err.Error(), "invalid_phone_number"):
log.Println("Phone number format invalid")
case strings.Contains(err.Error(), "number_not_verified"):
log.Println("Trial account: verify number first")
case strings.Contains(err.Error(), "insufficient_funds"):
log.Println("Account balance too low")
case strings.Contains(err.Error(), "unverified_caller"):
log.Println("From number not verified")
default:
log.Printf("Call error: %v", err)
}
}
Best Practices¶
- Use HTTPS webhooks - Twilio requires secure endpoints
- Validate signatures - Verify webhook authenticity
- Handle status callbacks - Track call progress
- Use E.164 format -
+15551234567for phone numbers - Set timeout on calls - Avoid billing for unanswered calls
- Monitor usage - Track minutes and SMS counts
Pricing¶
| Service | Price |
|---|---|
| Outbound Call | $0.0140/min (US) |
| Inbound Call | $0.0085/min (US) |
| SMS Outbound | $0.0079/segment (US) |
| SMS Inbound | $0.0079/segment (US) |
| Phone Number | $1.15/month (US) |
Prices vary by region. Check Twilio Pricing for current rates.
Next Steps¶
- Voice Calls Guide - Call handling patterns
- SMS Guide - SMS messaging patterns
- Voice Agents - Build conversational agents
- Telnyx - Alternative provider