webrtc-clients/ios/SKILL.md
Build VoIP calling apps on iOS using Telnyx WebRTC SDK. Covers authentication, making/receiving calls, CallKit integration, PushKit/APNS push notifications, call quality metrics, and AI Agent integration. Use when implementing real-time voice communication on iOS.
npx skillsauth add team-telnyx/telnyx-toolkit telnyx-webrtc-client-iosInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Build real-time voice communication into iOS applications using Telnyx WebRTC.
Prerequisites: Create WebRTC credentials and generate a login token using the Telnyx server-side SDK. See the
telnyx-webrtc-*skill in your server language plugin (e.g.,telnyx-python,telnyx-javascript).
pod 'TelnyxRTC', '~> 0.1.0'
Then run:
pod install --repo-update
https://github.com/team-telnyx/telnyx-webrtc-ios.gitmain branchDisable Bitcode: Build Settings → "Bitcode" → Set to "NO"
Enable Background Modes: Signing & Capabilities → +Capability → Background Modes:
Microphone Permission: Add to Info.plist:
<key>NSMicrophoneUsageDescription</key>
<string>Microphone access required for VoIP calls</string>
import TelnyxRTC
let telnyxClient = TxClient()
telnyxClient.delegate = self
let txConfig = TxConfig(
sipUser: "your_sip_username",
password: "your_sip_password",
pushDeviceToken: "DEVICE_APNS_TOKEN",
ringtone: "incoming_call.mp3",
ringBackTone: "ringback_tone.mp3",
logLevel: .all
)
do {
try telnyxClient.connect(txConfig: txConfig)
} catch {
print("Connection error: \(error)")
}
let txConfig = TxConfig(
token: "your_jwt_token",
pushDeviceToken: "DEVICE_APNS_TOKEN",
ringtone: "incoming_call.mp3",
ringBackTone: "ringback_tone.mp3",
logLevel: .all
)
try telnyxClient.connect(txConfig: txConfig)
| Parameter | Type | Description |
|-----------|------|-------------|
| sipUser / token | String | Credentials from Telnyx Portal |
| password | String | SIP password (credential auth) |
| pushDeviceToken | String? | APNS VoIP push token |
| ringtone | String? | Audio file for incoming calls |
| ringBackTone | String? | Audio file for ringback |
| logLevel | LogLevel | .none, .error, .warning, .debug, .info, .all |
| forceRelayCandidate | Bool | Force TURN relay (avoid local network) |
let serverConfig = TxServerConfiguration(
environment: .production,
region: .usEast // .auto, .usEast, .usCentral, .usWest, .caCentral, .eu, .apac
)
try telnyxClient.connect(txConfig: txConfig, serverConfiguration: serverConfig)
Implement TxClientDelegate to receive events:
extension ViewController: TxClientDelegate {
func onSocketConnected() {
// Connected to Telnyx backend
}
func onSocketDisconnected() {
// Disconnected from backend
}
func onClientReady() {
// Ready to make/receive calls
}
func onClientError(error: Error) {
// Handle error
}
func onIncomingCall(call: Call) {
// Incoming call while app is in foreground
self.currentCall = call
}
func onPushCall(call: Call) {
// Incoming call from push notification
self.currentCall = call
}
func onCallStateUpdated(callState: CallState, callId: UUID) {
switch callState {
case .CONNECTING:
break
case .RINGING:
break
case .ACTIVE:
break
case .HELD:
break
case .DONE(let reason):
if let reason = reason {
print("Call ended: \(reason.cause ?? "Unknown")")
print("SIP: \(reason.sipCode ?? 0) \(reason.sipReason ?? "")")
}
case .RECONNECTING(let reason):
print("Reconnecting: \(reason.rawValue)")
case .DROPPED(let reason):
print("Dropped: \(reason.rawValue)")
}
}
}
let call = try telnyxClient.newCall(
callerName: "John Doe",
callerNumber: "+15551234567",
destinationNumber: "+18004377950",
callId: UUID()
)
func onIncomingCall(call: Call) {
// Store reference and show UI
self.currentCall = call
// Answer the call
call.answer()
}
// End call
call.hangup()
// Mute/Unmute
call.muteAudio()
call.unmuteAudio()
// Hold/Unhold
call.hold()
call.unhold()
// Send DTMF
call.dtmf(digit: "1")
// Toggle speaker
// (Use AVAudioSession for speaker routing)
import PushKit
class AppDelegate: UIResponder, UIApplicationDelegate, PKPushRegistryDelegate {
private var pushRegistry = PKPushRegistry(queue: .main)
func initPushKit() {
pushRegistry.delegate = self
pushRegistry.desiredPushTypes = [.voIP]
}
func pushRegistry(_ registry: PKPushRegistry,
didUpdate credentials: PKPushCredentials,
for type: PKPushType) {
if type == .voIP {
let token = credentials.token.map { String(format: "%02X", $0) }.joined()
// Save token for use in TxConfig
}
}
func pushRegistry(_ registry: PKPushRegistry,
didReceiveIncomingPushWith payload: PKPushPayload,
for type: PKPushType,
completion: @escaping () -> Void) {
if type == .voIP {
handleVoIPPush(payload: payload)
}
completion()
}
}
func handleVoIPPush(payload: PKPushPayload) {
guard let metadata = payload.dictionaryPayload["metadata"] as? [String: Any] else { return }
let callId = metadata["call_id"] as? String ?? UUID().uuidString
let callerName = (metadata["caller_name"] as? String) ?? ""
let callerNumber = (metadata["caller_number"] as? String) ?? ""
// Reconnect client and process push
let txConfig = TxConfig(sipUser: sipUser, password: password, pushDeviceToken: token)
try? telnyxClient.processVoIPNotification(
txConfig: txConfig,
serverConfiguration: serverConfig,
pushMetaData: metadata
)
// Report to CallKit (REQUIRED on iOS 13+)
let callHandle = CXHandle(type: .generic, value: callerNumber)
let callUpdate = CXCallUpdate()
callUpdate.remoteHandle = callHandle
provider.reportNewIncomingCall(with: UUID(uuidString: callId)!, update: callUpdate) { error in
if let error = error {
print("Failed to report call: \(error)")
}
}
}
import CallKit
class AppDelegate: CXProviderDelegate {
var callKitProvider: CXProvider!
func initCallKit() {
let config = CXProviderConfiguration(localizedName: "TelnyxRTC")
config.maximumCallGroups = 1
config.maximumCallsPerCallGroup = 1
callKitProvider = CXProvider(configuration: config)
callKitProvider.setDelegate(self, queue: nil)
}
// CRITICAL: Audio session handling for WebRTC + CallKit
func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
telnyxClient.enableAudioSession(audioSession: audioSession)
}
func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
telnyxClient.disableAudioSession(audioSession: audioSession)
}
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
// Use SDK method to handle race conditions
telnyxClient.answerFromCallkit(answerAction: action)
}
func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
telnyxClient.endCallFromCallkit(endAction: action)
}
}
Enable with debug: true:
let call = try telnyxClient.newCall(
callerName: "John",
callerNumber: "+15551234567",
destinationNumber: "+18004377950",
callId: UUID(),
debug: true
)
call.onCallQualityChange = { metrics in
print("MOS: \(metrics.mos)")
print("Jitter: \(metrics.jitter * 1000) ms")
print("RTT: \(metrics.rtt * 1000) ms")
print("Quality: \(metrics.quality.rawValue)")
switch metrics.quality {
case .excellent, .good:
// Green indicator
case .fair:
// Yellow indicator
case .poor, .bad:
// Red indicator
case .unknown:
// Gray indicator
}
}
| Quality Level | MOS Range | |---------------|-----------| | .excellent | > 4.2 | | .good | 4.1 - 4.2 | | .fair | 3.7 - 4.0 | | .poor | 3.1 - 3.6 | | .bad | ≤ 3.0 |
client.anonymousLogin(
targetId: "your-ai-assistant-id",
targetType: "ai_assistant"
)
// After anonymous login, destination is ignored
let call = client.newInvite(
callerName: "User",
callerNumber: "user",
destinationNumber: "ai-assistant", // Ignored
callId: UUID()
)
let cancellable = client.aiAssistantManager.subscribeToTranscriptUpdates { transcripts in
for item in transcripts {
print("\(item.role): \(item.content)")
// role: "user" or "assistant"
}
}
let success = client.sendAIAssistantMessage("Hello, can you help me?")
class MyLogger: TxLogger {
func log(level: LogLevel, message: String) {
// Send to your logging service
MyAnalytics.log(level: level, message: message)
}
}
let txConfig = TxConfig(
sipUser: sipUser,
password: password,
logLevel: .all,
customLogger: MyLogger()
)
| Issue | Solution |
|-------|----------|
| No audio | Ensure microphone permission granted |
| Push not working | Verify APNS certificate in Telnyx Portal |
| CallKit crash on iOS 13+ | Must report incoming call to CallKit |
| Audio routing issues | Use enableAudioSession/disableAudioSession in CXProviderDelegate |
| Login fails | Verify SIP credentials in Telnyx Portal |
tools
Build cross-platform VoIP calling apps with React Native using Telnyx Voice SDK. High-level reactive API with automatic lifecycle management, CallKit/ConnectionService integration, and push notifications. Use for mobile VoIP apps with minimal setup.
tools
Build browser-based VoIP calling apps using Telnyx WebRTC JavaScript SDK. Covers authentication, voice calls, events, debugging, call quality metrics, and AI Agent integration. Use for web-based real-time communication.
tools
Build cross-platform VoIP calling apps with Flutter using Telnyx WebRTC SDK. Covers authentication, making/receiving calls, push notifications (FCM + APNS), call quality metrics, and AI Agent integration. Works on Android, iOS, and Web.
tools
Build VoIP calling apps on Android using Telnyx WebRTC SDK. Covers authentication, making/receiving calls, push notifications (FCM), call quality metrics, and AI Agent integration. Use when implementing real-time voice communication on Android.