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
- Purchase a phone number in Telnyx Portal
- Create a TeXML application or use Call Control API
- 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
- WebRTC - Browser-based calling
- Twilio - Alternative provider
- Audio Processing - Audio handling