Telnyx Voice Agent Integration

Step-by-step guide to integrate Telnyx with AI voice agents. WebSocket media streaming, global phone numbers, call control API setup, and real-time audio configuration.

Telnyx Integration

Telnyx provides global phone numbers with WebSocket media streaming for real-time voice agent interactions.

Overview

Feature Value
Media Protocol WebSocket
Audio Format PCM 16-bit
Sample Rate 8000 Hz
Regions Global
Best For International coverage

Configuration

Environment Variables

TELNYX_API_KEY=your-api-key
TELNYX_APP_ID=your-app-id
TELNYX_PUBLIC_KEY=your-public-key

Phone Number Setup

  1. Purchase a phone number in Telnyx Portal
  2. Create a TeXML application or use Call Control API
  3. Configure webhook URL for incoming calls

Implementation

Webhook Handler

type TelnyxHandler struct {
    apiKey    string
    sessions  map[string]*Session
}

func (h *TelnyxHandler) HandleWebhook(w http.ResponseWriter, r *http.Request) {
    var event TelnyxEvent
    json.NewDecoder(r.Body).Decode(&event)

    switch event.EventType {
    case "call.initiated":
        h.handleCallInitiated(event)
    case "call.answered":
        h.handleCallAnswered(event)
    case "call.hangup":
        h.handleCallHangup(event)
    case "streaming.started":
        h.handleStreamingStarted(event)
    case "streaming.stopped":
        h.handleStreamingStopped(event)
    }

    w.WriteHeader(http.StatusOK)
}

type TelnyxEvent struct {
    EventType string `json:"event_type"`
    Payload   struct {
        CallControlID string `json:"call_control_id"`
        CallLegID     string `json:"call_leg_id"`
        CallSessionID string `json:"call_session_id"`
        From          string `json:"from"`
        To            string `json:"to"`
        StreamURL     string `json:"stream_url"`
    } `json:"payload"`
}

Starting Media Streaming

func (h *TelnyxHandler) handleCallAnswered(event TelnyxEvent) {
    callControlID := event.Payload.CallControlID

    // Start media streaming
    req := StartStreamRequest{
        CallControlID: callControlID,
        StreamURL:     "wss://your-server.com/telnyx/media",
        StreamTrack:   "inbound_track", // or "both_tracks"
        EnableDialogflow: false,
    }

    h.telnyxClient.StartStreaming(req)

    // Create session
    h.sessions[callControlID] = NewSession(event)
}

func (h *TelnyxHandler) StartStreaming(req StartStreamRequest) error {
    url := fmt.Sprintf("https://api.telnyx.com/v2/calls/%s/actions/streaming_start",
        req.CallControlID)

    body := map[string]any{
        "stream_url":   req.StreamURL,
        "stream_track": req.StreamTrack,
    }

    jsonBody, _ := json.Marshal(body)

    httpReq, _ := http.NewRequest("POST", url, bytes.NewReader(jsonBody))
    httpReq.Header.Set("Authorization", "Bearer "+h.apiKey)
    httpReq.Header.Set("Content-Type", "application/json")

    resp, err := http.DefaultClient.Do(httpReq)
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    return nil
}

WebSocket Media Handler

type TelnyxMediaHandler struct {
    pipeline *Pipeline
}

func (h *TelnyxMediaHandler) HandleConnection(ws *websocket.Conn) {
    for {
        _, message, err := ws.ReadMessage()
        if err != nil {
            return
        }

        var msg TelnyxMediaMessage
        json.Unmarshal(message, &msg)

        switch msg.Event {
        case "connected":
            h.handleConnected(msg)
        case "start":
            h.handleStart(msg)
        case "media":
            h.handleMedia(msg, ws)
        case "stop":
            h.handleStop(msg)
        }
    }
}

type TelnyxMediaMessage struct {
    Event     string `json:"event"`
    StreamSid string `json:"stream_sid"`
    Media     struct {
        Track     string `json:"track"`
        Chunk     string `json:"chunk"`
        Timestamp string `json:"timestamp"`
        Payload   string `json:"payload"`
    } `json:"media"`
}

func (h *TelnyxMediaHandler) handleMedia(msg TelnyxMediaMessage, ws *websocket.Conn) {
    // Decode audio from base64
    audio, _ := base64.StdEncoding.DecodeString(msg.Media.Payload)

    // Process through pipeline
    h.pipeline.ProcessAudio(audio)

    // Send response audio
    h.sendAudio(ws, h.pipeline.GetOutputAudio())
}

func (h *TelnyxMediaHandler) sendAudio(ws *websocket.Conn, audio []byte) {
    msg := map[string]any{
        "event": "media",
        "media": map[string]string{
            "payload": base64.StdEncoding.EncodeToString(audio),
        },
    }

    jsonMsg, _ := json.Marshal(msg)
    ws.WriteMessage(websocket.TextMessage, jsonMsg)
}

Call Control API

Answer Call

func (c *TelnyxClient) AnswerCall(callControlID string) error {
    url := fmt.Sprintf("https://api.telnyx.com/v2/calls/%s/actions/answer",
        callControlID)

    body := map[string]any{}
    jsonBody, _ := json.Marshal(body)

    req, _ := http.NewRequest("POST", url, bytes.NewReader(jsonBody))
    req.Header.Set("Authorization", "Bearer "+c.apiKey)
    req.Header.Set("Content-Type", "application/json")

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    return nil
}

Hangup Call

func (c *TelnyxClient) HangupCall(callControlID string) error {
    url := fmt.Sprintf("https://api.telnyx.com/v2/calls/%s/actions/hangup",
        callControlID)

    body := map[string]any{}
    jsonBody, _ := json.Marshal(body)

    req, _ := http.NewRequest("POST", url, bytes.NewReader(jsonBody))
    req.Header.Set("Authorization", "Bearer "+c.apiKey)
    req.Header.Set("Content-Type", "application/json")

    resp, err := http.DefaultClient.Do(req)
    return err
}

Transfer Call

func (c *TelnyxClient) TransferCall(callControlID, destination string) error {
    url := fmt.Sprintf("https://api.telnyx.com/v2/calls/%s/actions/transfer",
        callControlID)

    body := map[string]any{
        "to": destination,
    }
    jsonBody, _ := json.Marshal(body)

    req, _ := http.NewRequest("POST", url, bytes.NewReader(jsonBody))
    req.Header.Set("Authorization", "Bearer "+c.apiKey)
    req.Header.Set("Content-Type", "application/json")

    resp, err := http.DefaultClient.Do(req)
    return err
}

Outbound Calls

func (c *TelnyxClient) DialCall(from, to, webhookURL string) (*Call, error) {
    url := "https://api.telnyx.com/v2/calls"

    body := map[string]any{
        "connection_id": c.connectionID,
        "to":            to,
        "from":          from,
        "webhook_url":   webhookURL,
        "answering_machine_detection": "detect",
    }
    jsonBody, _ := json.Marshal(body)

    req, _ := http.NewRequest("POST", url, bytes.NewReader(jsonBody))
    req.Header.Set("Authorization", "Bearer "+c.apiKey)
    req.Header.Set("Content-Type", "application/json")

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    var result struct {
        Data Call `json:"data"`
    }
    json.NewDecoder(resp.Body).Decode(&result)

    return &result.Data, nil
}

TeXML Alternative

TwiML-Compatible Markup

<?xml version="1.0" encoding="UTF-8"?>
<Response>
    <Connect>
        <Stream url="wss://your-server.com/telnyx/media" />
    </Connect>
</Response>

TeXML Handler

func (h *TelnyxHandler) HandleIncomingCall(w http.ResponseWriter, r *http.Request) {
    texml := `<?xml version="1.0" encoding="UTF-8"?>
<Response>
    <Connect>
        <Stream url="wss://your-server.com/telnyx/media" />
    </Connect>
</Response>`

    w.Header().Set("Content-Type", "application/xml")
    w.Write([]byte(texml))
}

Audio Format

Configuration

type TelnyxAudioConfig struct {
    Encoding   string // "PCMU" or "PCMA"
    SampleRate int    // 8000
    Channels   int    // 1
}

var DefaultAudioConfig = TelnyxAudioConfig{
    Encoding:   "PCMU", // μ-law
    SampleRate: 8000,
    Channels:   1,
}

Audio Processing

func (h *TelnyxMediaHandler) processInboundAudio(audio []byte) []byte {
    // Decode μ-law to linear PCM
    pcm := mulawDecode(audio)

    // Upsample to 16kHz for STT
    upsampled := resample(pcm, 8000, 16000)

    return upsampled
}

func (h *TelnyxMediaHandler) processOutboundAudio(audio []byte) []byte {
    // Downsample from TTS output
    downsampled := resample(audio, 24000, 8000)

    // Encode to μ-law
    mulaw := mulawEncode(downsampled)

    return mulaw
}

Error Handling

func (h *TelnyxHandler) handleError(callControlID string, err error) {
    log.Printf("Telnyx error for call %s: %v", callControlID, err)

    // Attempt to play error message
    h.speakAndHangup(callControlID,
        "I apologize, we're experiencing technical difficulties. Please try again later.")
}

func (h *TelnyxHandler) speakAndHangup(callControlID, message string) {
    url := fmt.Sprintf("https://api.telnyx.com/v2/calls/%s/actions/speak",
        callControlID)

    body := map[string]any{
        "payload": message,
        "voice":   "female",
        "language": "en-US",
    }
    jsonBody, _ := json.Marshal(body)

    req, _ := http.NewRequest("POST", url, bytes.NewReader(jsonBody))
    req.Header.Set("Authorization", "Bearer "+h.apiKey)
    req.Header.Set("Content-Type", "application/json")

    http.DefaultClient.Do(req)

    // Wait and hangup
    time.AfterFunc(5*time.Second, func() {
        h.HangupCall(callControlID)
    })
}

Best Practices

1. Handle Answering Machine Detection

func (h *TelnyxHandler) handleCallAnswered(event TelnyxEvent) {
    amdResult := event.Payload.AnsweringMachineDetection

    if amdResult == "machine_start" || amdResult == "machine_end_beep" {
        // It's a voicemail
        h.handleVoicemail(event)
        return
    }

    // Human answered
    h.startVoiceAgent(event)
}

2. Implement Call Recording

func (c *TelnyxClient) StartRecording(callControlID string) error {
    url := fmt.Sprintf("https://api.telnyx.com/v2/calls/%s/actions/record_start",
        callControlID)

    body := map[string]any{
        "format":   "mp3",
        "channels": "dual",
    }
    jsonBody, _ := json.Marshal(body)

    req, _ := http.NewRequest("POST", url, bytes.NewReader(jsonBody))
    req.Header.Set("Authorization", "Bearer "+c.apiKey)
    req.Header.Set("Content-Type", "application/json")

    resp, err := http.DefaultClient.Do(req)
    return err
}

Next Steps