# Plivo Voice Agent Integration

> Integrate Plivo telephony with AI voice agents. Cost-effective alternative to Twilio, WebSocket streaming, global phone numbers. Step-by-step setup guide with pricing comparison.

Source: https://edesy.in/docs/voice-agent/telephony/plivo

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

```bash
PLIVO_AUTH_ID=your_auth_id
PLIVO_AUTH_TOKEN=your_auth_token
PLIVO_APP_ID=your_application_id
```

### Plivo Application Setup

1. Create a Plivo Application in the console
2. Set the Answer URL to your webhook endpoint
3. 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

```go
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 Response Format

```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

```go
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

```go
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()
    }
}
```

## Audio Format

| 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):

```go
// 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

```go
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

```go
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

```go
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

```go
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

```go
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

```go
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

```go
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

```go
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

```go
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

- [Twilio Integration](https://edesy.in/docs/voice-agent/telephony/twilio) - Alternative provider
- [Exotel Integration](https://edesy.in/docs/voice-agent/telephony/exotel) - India-focused
- [WebSocket Protocol](https://edesy.in/docs/voice-agent/telephony) - Deep dive
