Plivo Integration
Plivo provides reliable telephony infrastructure with competitive pricing and global coverage, making it a strong alternative to Twilio.
Why Plivo?
| Feature |
Plivo |
Twilio |
| Cost per minute |
~$0.008 |
~$0.013 |
| Global coverage |
190+ countries |
180+ countries |
| WebSocket support |
Yes |
Yes |
| API simplicity |
Simple |
Complex |
| Support |
Good |
Excellent |
Configuration
Environment Variables
PLIVO_AUTH_ID=your_auth_id
PLIVO_AUTH_TOKEN=your_auth_token
PLIVO_APP_ID=your_application_id
Plivo Application Setup
- Create a Plivo Application in the console
- Set the Answer URL to your webhook endpoint
- Enable XML (PHLO not required)
Answer URL Configuration
Answer URL: https://your-server.com/api/plivo/answer
Fallback URL: https://your-server.com/api/plivo/fallback
Hangup URL: https://your-server.com/api/plivo/hangup
Implementation
Webhook Handler
func handlePlivoAnswer(w http.ResponseWriter, r *http.Request) {
// Parse incoming call parameters
callUUID := r.FormValue("CallUUID")
from := r.FormValue("From")
to := r.FormValue("To")
// Determine agent from called number
agent := getAgentByPhoneNumber(to)
// Return XML response with WebSocket stream
response := plivoxml.ResponseElement{}
response.AddStream(plivoxml.StreamElement{
StreamURL: fmt.Sprintf("wss://your-server.com/ws/plivo/%s", agent.ID),
Bidirectional: true,
AudioTrack: "inbound",
ContentType: "audio/x-l16;rate=8000",
})
xml, _ := response.ToXML()
w.Header().Set("Content-Type", "application/xml")
w.Write([]byte(xml))
}
<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Stream bidirectional="true"
audioTrack="inbound"
contentType="audio/x-l16;rate=8000">
wss://your-server.com/ws/plivo/agent-123
</Stream>
</Response>
WebSocket Handler
type PlivoWSHandler struct {
pipeline *Pipeline
conn *websocket.Conn
}
func (h *PlivoWSHandler) HandleConnection(w http.ResponseWriter, r *http.Request) {
// Upgrade to WebSocket
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
h.conn = conn
defer conn.Close()
// Extract agent ID from path
agentID := chi.URLParam(r, "agentID")
// Initialize pipeline
agent := getAgent(agentID)
h.pipeline = NewPipeline(agent, h)
// Start pipeline
ctx, cancel := context.WithCancel(r.Context())
defer cancel()
go h.pipeline.Run(ctx)
// Handle WebSocket messages
h.handleMessages()
}
func (h *PlivoWSHandler) handleMessages() {
for {
msgType, msg, err := h.conn.ReadMessage()
if err != nil {
return
}
if msgType == websocket.BinaryMessage {
// Audio data from caller
h.processAudio(msg)
} else if msgType == websocket.TextMessage {
// Control message
h.processControlMessage(msg)
}
}
}
Audio Processing
func (h *PlivoWSHandler) processAudio(audio []byte) {
// Plivo sends L16 PCM at 8kHz
samples := bytesToInt16LE(audio)
// Send to pipeline
h.pipeline.ProcessAudio(samples)
}
func (h *PlivoWSHandler) SendAudio(audio []byte) error {
// Convert to L16 format
l16Audio := int16ToBytes(audio)
// Send as binary message
return h.conn.WriteMessage(websocket.BinaryMessage, l16Audio)
}
func (h *PlivoWSHandler) processControlMessage(msg []byte) {
var event PlivoEvent
json.Unmarshal(msg, &event)
switch event.Event {
case "start":
log.Printf("Stream started: %s", event.StreamID)
h.streamID = event.StreamID
case "stop":
log.Printf("Stream stopped")
h.pipeline.Stop()
}
}
| Parameter |
Plivo Specification |
| Sample Rate |
8000 Hz |
| Encoding |
L16 (signed 16-bit little-endian PCM) |
| Channels |
Mono |
| Frame Size |
Variable (typically 20ms = 320 bytes) |
L16 vs μ-law
Unlike Twilio which uses μ-law, Plivo uses L16 (Linear PCM):
// No conversion needed for L16
func processL16Audio(audio []byte) []int16 {
samples := make([]int16, len(audio)/2)
for i := 0; i < len(samples); i++ {
samples[i] = int16(audio[i*2]) | int16(audio[i*2+1])<<8
}
return samples
}
// For output, convert back to bytes
func int16ToL16Bytes(samples []int16) []byte {
audio := make([]byte, len(samples)*2)
for i, s := range samples {
audio[i*2] = byte(s)
audio[i*2+1] = byte(s >> 8)
}
return audio
}
Outbound Calls
Initiate Call
func initiateOutboundCall(to, agentID string) (*PlivoCallResponse, error) {
client := plivo.NewClient(authID, authToken, nil)
response, err := client.Calls.Create(plivo.CallCreateParams{
From: plivoPhoneNumber,
To: to,
AnswerURL: fmt.Sprintf("https://your-server.com/api/plivo/outbound/%s", agentID),
AnswerMethod: "POST",
})
if err != nil {
return nil, err
}
return response, nil
}
Outbound Answer Handler
func handlePlivoOutbound(w http.ResponseWriter, r *http.Request) {
agentID := chi.URLParam(r, "agentID")
callUUID := r.FormValue("CallUUID")
// Store call details for WebSocket lookup
storeCallDetails(callUUID, agentID)
// Return WebSocket stream
response := plivoxml.ResponseElement{}
response.AddStream(plivoxml.StreamElement{
StreamURL: fmt.Sprintf("wss://your-server.com/ws/plivo/%s/%s", agentID, callUUID),
Bidirectional: true,
AudioTrack: "inbound",
ContentType: "audio/x-l16;rate=8000",
})
xml, _ := response.ToXML()
w.Header().Set("Content-Type", "application/xml")
w.Write([]byte(xml))
}
Call Transfer
func transferCall(callUUID, transferTo string) error {
client := plivo.NewClient(authID, authToken, nil)
// Update call with transfer
_, err := client.Calls.Update(callUUID, plivo.CallUpdateParams{
LegsID: "aleg",
AlegURL: fmt.Sprintf("https://your-server.com/api/plivo/transfer?to=%s", transferTo),
AlegMethod: "POST",
})
return err
}
func handleTransfer(w http.ResponseWriter, r *http.Request) {
to := r.URL.Query().Get("to")
response := plivoxml.ResponseElement{}
response.AddDial(plivoxml.DialElement{
Children: []interface{}{
plivoxml.NumberElement{Number: to},
},
})
xml, _ := response.ToXML()
w.Header().Set("Content-Type", "application/xml")
w.Write([]byte(xml))
}
DTMF Handling
func (h *PlivoWSHandler) processControlMessage(msg []byte) {
var event PlivoEvent
json.Unmarshal(msg, &event)
switch event.Event {
case "dtmf":
digit := event.Digit
log.Printf("DTMF received: %s", digit)
h.pipeline.HandleDTMF(digit)
}
}
Error Handling
func handlePlivoFallback(w http.ResponseWriter, r *http.Request) {
errorType := r.FormValue("ErrorType")
log.Printf("Plivo fallback triggered: %s", errorType)
// Return a simple response or transfer to backup
response := plivoxml.ResponseElement{}
response.AddSpeak(plivoxml.SpeakElement{
Text: "We're experiencing technical difficulties. Please try again later.",
})
response.AddHangup(plivoxml.HangupElement{})
xml, _ := response.ToXML()
w.Header().Set("Content-Type", "application/xml")
w.Write([]byte(xml))
}
Webhooks
Hangup Webhook
func handlePlivoHangup(w http.ResponseWriter, r *http.Request) {
callUUID := r.FormValue("CallUUID")
duration := r.FormValue("Duration")
hangupCause := r.FormValue("HangupCause")
log.Printf("Call ended: %s, duration: %s, cause: %s",
callUUID, duration, hangupCause)
// Clean up session
cleanupSession(callUUID)
// Store call record
storeCallRecord(CallRecord{
UUID: callUUID,
Duration: parseInt(duration),
Cause: hangupCause,
})
w.WriteHeader(http.StatusOK)
}
Cost Comparison
| Route |
Plivo |
Twilio |
| US Local |
$0.008/min |
$0.013/min |
| US Toll-Free |
$0.018/min |
$0.022/min |
| India |
$0.003/min |
$0.0085/min |
| UK |
$0.009/min |
$0.012/min |
| Phone Numbers |
$0.80/mo |
$1.00/mo |
Best Practices
1. Use Connection Pooling
var plivoClient = plivo.NewClient(authID, authToken, &plivo.ClientOptions{
HTTPClient: &http.Client{
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 90 * time.Second,
},
},
})
2. Implement Retries
func callWithRetry(params plivo.CallCreateParams) (*plivo.CallCreateResponse, error) {
maxRetries := 3
backoff := 500 * time.Millisecond
for i := 0; i < maxRetries; i++ {
resp, err := plivoClient.Calls.Create(params)
if err == nil {
return resp, nil
}
if !isRetryable(err) {
return nil, err
}
time.Sleep(backoff)
backoff *= 2
}
return nil, fmt.Errorf("max retries exceeded")
}
3. Monitor Call Quality
func logCallMetrics(callUUID string) {
// Get call details
call, _ := plivoClient.Calls.Get(callUUID)
metrics.RecordHistogram("plivo.call.duration", float64(call.Duration))
metrics.RecordCounter("plivo.calls.total", 1)
if call.HangupCause != "NORMAL_CLEARING" {
metrics.RecordCounter("plivo.calls.failed", 1)
}
}
Next Steps