Telnyx provider implementation for OmniVoice - the voice abstraction layer for PlexusOne.
- 📞 CallSystem: PSTN call handling via Telnyx Call Control API
- 📡 Transport: Telnyx Media Streaming for real-time audio
- 🎙️ Voice Gateway: Full STT→LLM→TTS pipeline with registry integration
- 🔌 Provider Injection: Inject your own LLM providers for full SDK access
- 💬 SMS/MMS/RCS: Messaging via SMSProvider and OmniChat interfaces
go get github.com/plexusone/omni-telnyxpackage main
import (
"context"
"fmt"
"log"
"github.com/plexusone/omni-telnyx/omnivoice/callsystem"
)
func main() {
// Create Telnyx provider
provider, err := callsystem.New(
callsystem.WithAPIKey("YOUR_TELNYX_API_KEY"),
callsystem.WithPhoneNumber("+15551234567"),
callsystem.WithConnectionID("your-connection-id"),
callsystem.WithWebhookURL("https://your-server.com/webhooks"),
)
if err != nil {
log.Fatal(err)
}
defer provider.Close()
// Make outbound call
call, err := provider.MakeCall(context.Background(), "+15559876543")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Call initiated: %s\n", call.ID())
}To send keypad tones (DTMF) once an outbound call is answered, for example to join a meeting's phone dial-in line, pass WithSendDigits from omnivoice-core's callsystem package:
import corecallsystem "github.com/plexusone/omnivoice-core/callsystem"
// Wait 4 seconds, enter the meeting ID and #, wait 8 seconds, then enter
// * and the passcode and #.
call, err := provider.MakeCall(ctx, "+16695550100",
corecallsystem.WithSendDigits("WWWW85012345678#WWWWWWWW*482913#"),
)Digits use dial-string notation: 0-9, *, #, and A-D are tones, w waits half a second, and W waits one second. Invalid digits return an error before the call is placed.
The digits are sent with Telnyx's send_dtmf command when the call is answered. The voice gateway does this on the call.answered webhook; when you use the call system without the gateway, call SendPendingDigits on the *callsystem.Call when you receive call.answered. Digits are sent once per call. Through the gateway, use gw.MakeCallWithOptions(ctx, to, opts...).
import (
"net/http"
"github.com/plexusone/omnivoice-core/callsystem"
telnyxcs "github.com/plexusone/omni-telnyx/omnivoice/callsystem"
)
func main() {
provider, _ := telnyxcs.New(
telnyxcs.WithAPIKey("YOUR_TELNYX_API_KEY"),
telnyxcs.WithPhoneNumber("+15551234567"),
)
// Set incoming call handler
provider.OnIncomingCall(func(call callsystem.Call) error {
fmt.Printf("Incoming call from %s\n", call.From())
return call.Answer(context.Background())
})
// Handle Telnyx webhooks
http.HandleFunc("/webhooks", func(w http.ResponseWriter, r *http.Request) {
// Parse Telnyx webhook payload
// callControlID, from, to := parseWebhook(r)
// provider.HandleIncomingWebhook(callControlID, from, to)
})
http.ListenAndServe(":8080", nil)
}import "github.com/plexusone/omni-telnyx/omnivoice/transport"
// Create transport for Media Streaming
tr, _ := transport.New()
// Listen for Media Streaming connections
connCh, _ := tr.Listen(ctx, "/media-stream")
// Handle WebSocket upgrades in your HTTP handler
http.HandleFunc("/media-stream", func(w http.ResponseWriter, r *http.Request) {
tr.HandleWebSocket(w, r, "/media-stream")
})
// Process connections
for conn := range connCh {
go func(c transport.Connection) {
// Read audio from caller
audio := make([]byte, 1024)
for {
n, err := c.AudioOut().Read(audio)
if err != nil {
break
}
// Process audio...
// Send audio back
c.AudioIn().Write(responseAudio)
}
}(conn)
}By default the gateway runs a voice pipeline (STT, LLM, TTS) on every call. Set Mode: coregateway.PipelineModeAudio (from github.com/plexusone/omnivoice-core/gateway) to run no pipeline and handle the call's audio yourself, for example to record a call or to build an agent that attends a meeting by phone and speaks only when addressed:
sess, err := gw.MakeCallWithOptions(ctx, "+16695550100")
if err != nil {
return err
}
audio := sess.(coregateway.AudioSession)
go func() {
for chunk := range audio.ReceiveAudio() { // mu-law (PCMU), 8 kHz, mono
recorder.Write(chunk)
}
}()
if err := audio.SendAudio(mulawSpeech); err != nil {
return err
}In audio mode the gateway starts Media Streaming with bidirectional PCMU RTP, so audio sent with SendAudio plays on the call. ReceiveAudio carries the remote party's audio and is closed when the media stream stops. To end the call, call Hangup(ctx) on the session: Close stops only the media stream and leaves a Telnyx call connected. SendAudio returns ErrAudioNotConnected before the stream connects and ErrSessionClosed after the call ends; ClearAudio stops audio that has not played yet.
When you use the call system without the gateway, pass callsystem.WithBidirectionalPCMU() to StartMediaStreaming to send mu-law audio back over the stream.
// After call is answered...
call, _ := provider.MakeCall(ctx, "+15559876543")
// Start media streaming
telnyxCall := call.(*telnyxcs.Call)
telnyxCall.StartMediaStreaming(ctx, "wss://your-server.com/media-stream")
// Use built-in TTS
telnyxCall.Speak(ctx, "Hello, how can I help you?", "", "")
// Play audio file
telnyxCall.PlayAudio(ctx, "https://example.com/audio.mp3")
// Start real-time transcription
telnyxCall.StartTranscription(ctx, "en")
// Stop transcription
telnyxCall.StopTranscription(ctx)
// Hang up
call.Hangup(ctx)// Send SMS using default number
msg, err := provider.SendSMS(ctx, "+15559876543", "Hello from OmniVoice!")
// Send SMS from specific number
msg, err = provider.SendSMSFrom(ctx, "+15559876543", "+15551234567", "Hello!")
fmt.Printf("Message sent: %s\n", msg.ID)Use omnivoice-core's provider registry for automatic discovery:
import (
omnivoice "github.com/plexusone/omnivoice-core"
"github.com/plexusone/omnivoice-core/registry"
_ "github.com/plexusone/omni-telnyx/omnivoice/gateway" // Auto-register
)
// Get gateway via registry
gw, err := omnivoice.GetGatewayProvider("telnyx",
registry.WithAPIKey("TELNYX_API_KEY"),
registry.WithExtension("phoneNumber", "+15551234567"),
)Inject your own thick LLM provider instead of using the default:
import (
"github.com/plexusone/omni-telnyx/omnivoice/gateway"
"github.com/plexusone/omnillm" // Your thick provider
)
// Create LLM client with full SDK access
llmClient, _ := omnillm.NewClient(omnillm.ClientConfig{
Providers: []omnillm.ProviderConfig{{
Provider: "anthropic",
APIKey: os.Getenv("ANTHROPIC_API_KEY"),
}},
})
// Inject into gateway config
gw, err := gateway.New(gateway.Config{
APIKey: "TELNYX_API_KEY",
LLMClient: llmClient,
})export TELNYX_API_KEY="your-api-key"provider, _ := callsystem.New(
callsystem.WithAPIKey("your-api-key"),
callsystem.WithPhoneNumber("+15551234567"), // Default outbound number
callsystem.WithConnectionID("connection-id"), // Telnyx Connection ID
callsystem.WithWebhookURL("https://..."), // Webhook URL for events
)┌─────────────────────────────────────────────────────────────┐
│ Phone Call Flow │
├─────────────────────────────────────────────────────────────┤
│ │
│ Caller ←→ Telnyx PSTN ←→ Media Streaming ←→ Your Server │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │
│ │ CallSystem │ │ Transport │ │ Agent │ │
│ │ (calls) │←──→│ (audio) │←──→│ (TTS/STT) │ │
│ └─────────────┘ └─────────────┘ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
Every call has a unique call_control_id used to send commands via the Call Control API.
A Telnyx Connection represents a configuration for handling calls (SIP, PSTN, etc.). Required for outbound calls.
Real-time bidirectional audio via WebSocket. Audio is base64-encoded mu-law or a-law at 8kHz.
- Go 1.21+
- Telnyx Account with API Key
- Telnyx Connection ID (for outbound calls)
- Public webhook URL for call events
- WebSocket endpoint for Media Streaming
Run conformance tests:
# Unit tests (no API key required)
go test ./...
# Integration tests (requires API key)
export TELNYX_API_KEY="your-api-key"
export TELNYX_PHONE_NUMBER="+15551234567"
export TELNYX_TO_NUMBER="+15559876543"
export TELNYX_CONNECTION_ID="your-connection-id"
go test -tags integration ./...- omnivoice-core - Core interfaces
- twilio-go - Twilio provider
- elevenlabs-go - ElevenLabs SDK with OmniVoice provider
MIT