# Telnyx Calling: WebRTC — Full Documentation > Complete page content for WebRTC (Calling section) of the Telnyx developer docs (https://developers.telnyx.com). > This file: https://developers.telnyx.com/docs/development/llms/calling-webrtc-llms-full-txt.md · Root index: https://developers.telnyx.com/llms.txt ## Getting Started ### Fundamentals > Source: https://developers.telnyx.com/docs/voice/webrtc/fundamentals.md ## What & Why These SDKs enable client-side applications to instantiate and control a Telnyx call leg. As a result, developers of applications integrated with Telnyx voice platform are no longer constrained to working with inflexible and uncustomizable SIP UAs such as PBX, Asterisk, Zoiper etc. Instead they can embed native voice capabilities client-side to work seamlessly with their voice application and achieve end to end visibility and control of the user experience. ## How These SDKs * Utilize the native client-end (browser or device) WebRTC API for cross browser/device compatibility, … * Adhere to the WebRTC standardization where Media is transported via RTP over DTLS, aka SRTP, aka DTLS-SRTP, … and * Implements the WebRTC session negotiation, aka signaling, via JSON-RPC messages over Secure WebSocket (WSS). ## Availability The following SDKs are offered * [Javascript SDK](https://github.com/team-telnyx/webrtc) * [Native iOS SDK](https://github.com/team-telnyx/telnyx-webrtc-ios) * [Native Android SDK](https://github.com/team-telnyx/telnyx-webrtc-android) * [Flutter SDK](https://github.com/team-telnyx/flutter-voice-sdk) --- ### Architecture > Source: https://developers.telnyx.com/docs/voice/webrtc/architecture.md To properly architect solutions and/or troubleshoot issues, one must understand how WebRTC Voice SDK fits among Telnyx's product portfolio. ![](/assets/images/webrtc-voicesdk-architecture.png) ```mermaid flowchart LR A[Browser / Mobile App] -->|WebRTC| B[rtc.telnyx.com] B -->|SIP| C[Telnyx SIP Platform] C -->|PSTN| D[Phone Network] E[Your Backend] -->|Call Control API| C C -->|Webhooks| E ``` This is explained in the following set of statements — ## WebRTC Voice SDKs CANNOT be used on its own for calling They merely lower the barriers for users to incorporate voice functionalities in their applications, i.e. instantiate a call leg. `rtc.telnyx.com` acts as the translation layer where on the SDK facing side, it adheres to the WebRTC standard and on the SIP facing side, speaks SIP protocol. To the core SIP platform, `rtc.telnyx.com` is merely another SIP UA. This is clearly illustrated by the fact that all methods of authenticating an SDK client are based on [SIP connection](https://developers.telnyx.com/docs/voice/webrtc/sdk-commonalities#authentication). This setup … * avails WebRTC Voice SDKs the worldwide PSTN calling coverage and, more importantly, * puts those calls under the umbrella of Programmable Voice API. ## WebRTC Voice SDKs CANNOT be used on its own to orchestrate call flow They merely allow some form of local control, e.g. un/hold, un/mute, sending DTMF digits. To orchestrate call flow or manipulate audio, TeXML or Call Control API must be used. Consider this example – a simple prepaid calling app where the user is told the remaining number of minutes before the call is placed. In the case of inadequate balance, they are told to top up before the call is hung up gracefully. The Voice SDKs are insufficient to achieve this simple call flow on their own. Instead, it is necessary to incorporate call control API — * The call leg instantiated by the SDK must be parked via a setting on the SIP connection. * The user’s backend must * respond to Telnyx webhooks, * inject the necessary custom Text-To-Speach audio, * place another outbound leg to the intended PSTN destination (or hangup due to insufficient balance), and finally, * bridge the WebRTC call leg with the PSTN leg ## WebRTC SDKs’ role in the Telnyx Voice Product Suite To conclude, WebRTC SDKs’ role in the Telnyx voice product suite is one where * They bring the Telnyx voice infrastructure closer to the ultimate end users. Developers do not need to maintain their own voice infrastructure. Instead, they can focus on building user facing applications and business logic. * They lower the barrier to access Telnyx’s worldwide PSTN coverage. Developers do not need to know SIP. Instead, they can work with the widely adopted WebRTC standardization and API. * They unify all the crucial building blocks of a CPaaS platform under the Telnyx umbrella. Developers do not need to manage multiple integrations and vendors in their stack. --- ### SDK Commonalities > Source: https://developers.telnyx.com/docs/voice/webrtc/sdk-commonalities.md ## Classes, Methods, and Events ***Broadly speaking***, across all the SDKs — There are two main classes — * The Client class that represents the session. This session encapsulates the websocket connection which is used for signaling and the active call. * The Call class that represents a webRTC media connection The Client class offers methods to * Instantiate an outbound call * Un/Register callback handlers for events * Control input and output devices The Call class offers methods to perform actions on a call, e.g. * Answer or hang up * Emit DTMF digits There are three categories of events exposed — * On changes to the websocket, e.g. connected or disconnected * On changes to the client, e.g. ready to make and receive calls * On changes to the call, e.g. answered ## Call States Every SDK exposes a set of call states that describe where a call is in its lifecycle. The diagram below shows the common state machine shared across all WebRTC SDKs: ```mermaid stateDiagram-v2 [*] --> NEW NEW --> CONNECTING : Outbound call NEW --> RINGING : Inbound call CONNECTING --> ACTIVE : Call answered RINGING --> ACTIVE : Call answered ACTIVE --> HELD : Hold HELD --> ACTIVE : Unhold ACTIVE --> DONE : Hangup HELD --> DONE : Hangup CONNECTING --> DONE : Rejected / Timeout RINGING --> DONE : Rejected / Timeout ``` Some platforms define additional states beyond the common set. iOS and Android add **RECONNECTING** and **DROPPED** (with an associated reason) for network-recovery scenarios. Flutter and Android add an **ERROR** state for unrecoverable failures. ## Authentication A Client instance needs to be properly authenticated before a call can be made or received. The following means of authentications are offered * [Basic credential based SIP connection](https://developers.telnyx.com/docs/voice/webrtc/auth/credential-connections) * [Telephony credential](https://developers.telnyx.com/docs/voice/webrtc/auth/telephony-credentials) * [JWT](https://developers.telnyx.com/docs/voice/webrtc/auth/jwt) Consult the linked guides on to the specific how-to guides. ## Dialing Registered Clients Method of Authentication Dialing registered clients with Examples Basic credential based SIP connection SIP user name on the connection object john1234@sip.telnyx.com Basic credential based SIP connection Phone number on the connection (* See notes below.) +13128889999 Telephony credential SIP user name on the telephony credential object gencredXXXYYY@sip.telnyx.com JWT SIP user name on the parent telephony credential object gencredxXxYyY@sip.telnyx.com Dialing registered client using phone number on the connection requires "Destination Number Format" to be set as "SIP Username" on the "Inbound" setting of the same connection. ## Multi-client Registration Behavior It’s recommended that the user sticks to one method of authentication and not mix and match unless there is a compelling use case for it. Here is an example to illustrate — Credential based SIP connection with SIP username `john1234`. Attached to this connections are: * Telephony credential, `gencred1` * JWT, `token1_1` * Telephony credential, `gencred2` * JWT, `token2_1` * JWT, `token2_2` Respective registrations are: * `client_a` is registered with `john1234` * `client_b` is registered with `gencred1` * `client_c` is registered with `token1_1` * `client_d` is registered with `gencred2` * `client_e` is registered with `token2_1` * `client_f` is registered with `token2_2` Dialing… Which client gets rung… john1234@sip.telnyx.com client_a gencred1@sip.telnyx.com Indeterminate; the last client to register between client_b and client_c. gencred2@sip.telnyx.com Indeterminate; the last client to register between client_d, client_e and client_f. ## Common Usage Patterns Two common primitive patterns are presented below. They can be augmented or used in combination with each other to achieve the user’s desired call flows. ### Pattern 1 This pattern is driven by the client-end application. * A client-end application (Web or Mobile App) initiates a call. * The call is temporarily parked by Telnyx. * Telnyx issues a webhook event to the user’s backend service. * User’s backend service performs additional processing using Telnyx Voice API, TeXML or Conferencing API. * Depending on user’s business logic, * a second call leg may be initiated by the user’s backend and bridged to the initial call leg, or * the initial call leg be put into a queue or conference until bridged to another call leg. ### Pattern 2 This pattern is driven by a call from outside the Telnyx network. * Telnyx receives a call from outside the Telnyx network, e.g. PSTN. * Telnyx processes the call via TeXML instruction or Voice API commands * That call leg is placed into a queue or conference room * User’s backend service initiates a second call leg toward a client-end application * The two call legs are eventually joined via bridge command or conference join ## Costs WebRTC call legs are billed at $0.002/minute. Other voice legs and add on features are charged separately and independently according to the user’s price plan. --- ## Authentication ### Credential Connections > Source: https://developers.telnyx.com/docs/voice/webrtc/auth/credential-connections.md ## Prerequisites * A valid V2 API key ## Creating a Credential Based SIP Connection The following API request will create a basic credential based SIP connection. ```http POST /v2/credential_connections HTTP/1.1 Host: api.telnyx.com Content-Type: application/json Authorization: Bearer XXX Content-Length: 169 { "active": true, "password": "xxx", "user_name": "myagent01", "anchorsite_override": "Latency", "connection_name": "parent-sip-connection" } ``` For call flows that make use of Pattern 1 (See [Common Usage Patterns](https://developers.telnyx.com/docs/voice/webrtc/fundamentals#common-usage-patterns)), the following additional configuration is required. ``` PATCH /v2/credential_connections/:id HTTP/1.1 Host: api.telnyx.com Content-Type: application/json Authorization: Bearer XXX Content-Length: 169 { "webhook_event_url": "https://mywebhook.com/primary", "webhook_event_failover_url": "https://mywebhook.com/backup", "webhook_api_version": "2", "webhook_timeout_secs": 25, "outbound": { "call_parking_enabled": true, "outbound_voice_profile_id": "123412415234124" } } ``` For call flows that make use of Pattern 2 (See [Common Usage Patterns](https://developers.telnyx.com/docs/voice/webrtc/fundamentals#common-usage-patterns)), the following configuration is required. ``` PATCH /v2/credential_connections/:id HTTP/1.1 Host: api.telnyx.com Content-Type: application/json Authorization: Bearer XXX Content-Length: 169 { "sip_uri_calling_preference": "internal" } ``` ## Using This Connection with Telephony Credentials For WebRTC SDK authentication, this connection is typically the parent resource for one or more telephony credentials. Best practices: * For multi-user applications, create a separate telephony credential per device * If you create telephony credentials on demand, wait about 5 seconds before the first login. The same applies to a JWT minted from that credential * If you use JWT authentication, mint the JWT from that device's telephony credential * Use the telephony credential's `sip_username` (`gencred...`) with the SIP Registration Status endpoint to check whether the SDK is currently registered ## SDK Authentication SDKs are authenticated with * `user_name` * `password` ## Limits Sum of the following may not exceed 10,000 for an account. * Count of credential connection * Count of IP connection * Count of FQDN connection * Count of external connection * Count of TeXML application * Count of Call Control Application ## Additional Resources * [Credential SIP Connections API Reference](https://developers.telnyx.com/api-reference/credential-connections/create-a-credential-connection#create-a-credential-connection) * [SIP Registration Status API Reference](https://developers.telnyx.com/api-reference/uac-connections/sip-registration-status) --- ### Telephony Credentials > Source: https://developers.telnyx.com/docs/voice/webrtc/auth/telephony-credentials.md ## Prerequisites * An active credential based SIP connection ## Create a Credential The following API request will create a telephony credential. ```http POST /v2/telephony_credentials HTTP/1.1 Host: api.telnyx.com Content-Type: application/json Authorization: Bearer XXX Content-Length: 75 { "connection_id": "1567510696929005999", "expires_at": "2024-09-18T00:00:00", "name": "contact-center-1", "tag": "sandbox" } ``` * `connection_id` is required * `expires_at` is recommended for security especially when many are expected to be created * `name` and `tag` are recommended for easy management Multiple telephony credentials can be created on a single connection. ## Propagation Time and Immediate Use Telephony credential creation is not guaranteed to be immediately usable for SDK login or registration. In create-then-login flows, the first authentication attempt can fail transiently even though the API request succeeded. Best practices: * Prefer creating credentials ahead of time when possible * If you create credentials on demand, wait about 5 seconds before the first login or registration attempt * If you cannot wait, retry with short exponential backoff and treat early failures as transient ## Updating a Credential After a credential's creation, it may be updated via the PATCH endpoint. ```http PATCH /v2/telephony_credentials/:id HTTP/1.1 Host: api.telnyx.com Content-Type: application/json Authorization: Bearer XXX Content-Length: 83 { "expires_at": "2024-09-11T21:07:00" } ``` The following error will be returned when trying to perform updates on an `expired` credential since that state is terminal. ```http { "errors": { "status": "can't update credentials in expired status" } } ``` An expired credential can only be deleted. ## Revoking a Credential A client-side application’s voice capabilities can be revoked by removing the corresponding credential. ```http DELETE /v2/telephony_credentials/:id HTTP/1.1 Host: api.telnyx.com Content-Type: application/json Authorization: Bearer XXX ``` ## Managing Credentials The following filters are useful when managing many credentials. * `filter[resource_id]` e.g. `filter[resource_id]=connection:1567510696929005999`. Note that `connection:` must be prepended to the connection ID. * `filter[status]` e.g. `filter[status]=expired` * `filter[status]` e.g. `filter[tag]=sandbox` ```http GET /v2/telephony_credentials?filter[status]=expired&filter[tag]=sandbox HTTP/1.1 Host: api.telnyx.com Authorization: Bearer XXX ``` ## How Telephony Credentials Should Be Used A telephony credential is a SIP identity for one SDK device. Best practices: * Create a separate telephony credential for each device * Do not share one telephony credential across concurrent devices * JWTs minted from the same telephony credential still represent the same SIP identity ## SDK Authentication SDKs are authenticated with * `sip_username` which starts with `gencred` * `sip_password` ## Check SIP Registration Status After an SDK client logs in, you can verify whether the underlying telephony credential is currently registered. ```http GET /v2/sip_registration_status?credential_type=telephony_credential&username=gencredabc123 HTTP/1.1 Host: api.telnyx.com Authorization: Bearer XXX ``` Use the credential's `sip_username` (`gencred...`) as `username`. ## Limits Currently, there exists * No limit on count of telephony credentials on a connection, * Nor any limit on the aggregate count of telephony credentials on a single account. ## Additional Resources * [Telephony Credentials API Reference](https://developers.telnyx.com/docs/voice/webrtc/auth/telephony-credentials/index#create-a-credential) * [SIP Registration Status API Reference](https://developers.telnyx.com/api-reference/uac-connections/sip-registration-status) --- ### JWTs > Source: https://developers.telnyx.com/docs/voice/webrtc/auth/jwt.md ## Prerequisites * An active telephony credential ## Create a Token The following API request will generate a JWT. ```http POST /v2/telephony_credentials/:id/token HTTP/1.1 Host: api.telnyx.com Authorization: Bearer XXX ``` This JWT is valid until: * 24 hours after its creation or * the parent telephony credential is expired whichever comes first ## What a JWT Represents A JWT is an authentication token for one telephony credential. It does not create a new SIP identity. Best practices: * JWTs minted from the same telephony credential still represent the same `sip_username` (`gencred...`) * Create a separate telephony credential per device, then mint JWTs from that credential ## Immediate Login After Credential Creation If you create a telephony credential and mint a JWT from it, wait about 5 seconds before using either the `gencred` or the JWT for login. Using them immediately after creation can fail transiently while the credential propagates. ## SDK Authentication SDKs are authenticated with the JWT. ## Check SIP Registration Status If you need to confirm whether the SDK is currently registered, use the underlying telephony credential's `sip_username` (`gencred...`) with the SIP Registration Status endpoint. ```http GET /v2/sip_registration_status?credential_type=telephony_credential&username=gencredabc123 HTTP/1.1 Host: api.telnyx.com Authorization: Bearer XXX ``` ## Limits Currently, there exists * No limit on count of tokens on a telephony credential, * Nor any limit on the aggregate count of tokens on a single account. ## Additional Resources * [JWT API Reference](https://developers.telnyx.com/docs/voice/webrtc/auth/jwt/index#create-a-token) * [SIP Registration Status API Reference](https://developers.telnyx.com/api-reference/uac-connections/sip-registration-status) --- ## Push Notifications ### Overview > Source: https://developers.telnyx.com/docs/voice/webrtc/push-notifications.md ## How push notifications work When a client connects to the Telnyx WebRTC platform, it maintains a WebSocket connection that receives incoming call invitations in real time. If the app moves to the background or the device terminates it, that socket closes and calls can no longer reach the device. Push notifications bridge this gap. During login the SDK registers a platform-specific push token (FCM for Android, APNS for iOS) with Telnyx. When an incoming call targets that user, Telnyx sends a push notification through the appropriate service. The device wakes the app, which reconnects to the socket and receives the actual call invitation. ``` Caller ──▶ Telnyx Platform ──▶ FCM / APNS ──▶ Device │ App wakes up │ Reconnects WebSocket │ Receives call invitation ``` ## Multidevice support A single user can register up to **5 push tokens** across iOS (APNS) and Android (FCM) devices. Each time a user logs in and provides a push token, Telnyx registers it. If a sixth token is added, the least-recently-used token is removed. This means up to five devices can receive push notifications for the same incoming call simultaneously. ## Platform setup Push notification configuration has two parts: 1. **Portal setup** — Create a push credential in the Telnyx Portal and attach it to a SIP Connection. 2. **App setup** — Integrate the push notification service into your application code and pass the token to the SDK on login. Each platform has its own requirements: | Platform | Push service | Credential type | Guide | | --- | --- | --- | --- | | Android | Firebase Cloud Messaging (FCM) | Android Credential (service account JSON) | [Android guide](/docs/voice/webrtc/push-notifications/android) | | iOS | Apple Push Notification Service (APNS) | iOS Credential (cert.pem + key.pem) | [iOS guide](/docs/voice/webrtc/push-notifications/ios) | | Flutter | FCM (Android) + APNS (iOS) | Both credentials required | [Flutter guide](/docs/voice/webrtc/push-notifications/flutter) | | React Native | FCM (Android) + APNS (iOS) | Both credentials required | [React Native guide](/docs/voice/webrtc/push-notifications/react-native) | ## API reference You can also manage push credentials programmatically through the API: - [Mobile Push Credentials API](/api/webrtc/mobile-push-credentials) --- ### Android > Source: https://developers.telnyx.com/docs/voice/webrtc/push-notifications/android.md ## Prerequisites - A [Telnyx account](https://portal.telnyx.com) with a configured SIP Connection - A [Firebase project](https://console.firebase.google.com/) with Cloud Messaging enabled - The Telnyx Android WebRTC SDK integrated into your application ## Portal setup ### 1. Configure Firebase Cloud Messaging 1. Go to the [Firebase Console](https://console.firebase.google.com/) and open your project. 2. Navigate to **Project Overview → Project Settings → Service Accounts**. 3. Select **Generate New Private Key** to download a service account JSON file. The Firebase Cloud Messaging HTTP v1 API uses a service account JSON key, not the legacy server key. Make sure you download the full service account JSON file. ### 2. Create an Android push credential in the Telnyx Portal 1. Go to [portal.telnyx.com](https://portal.telnyx.com) and log in. 2. Navigate to **API Keys** in the left panel. 3. Select the **Credentials** tab, then click **Add → Android Credential**. 4. Enter a credential name and paste the contents of the service account JSON file into the **Project Account JSON** field. 5. Click **Add Push Credential** to save. ### 3. Attach the credential to a SIP Connection 1. Navigate to **SIP Connections** in the left panel. 2. Open the SIP Connection you want to configure (or [create a new one](/docs/voice/sip-trunking/get-started)). 3. Select the **WebRTC** tab. 4. In the **Android** section, select the push credential you created. 5. Save the SIP Connection. --- ## App setup ### Retrieve the FCM token After integrating Firebase into your Android application ([Firebase setup guide](https://firebase.google.com/docs/android/setup)), retrieve the FCM registration token: ```kotlin private fun getFCMToken() { FirebaseApp.initializeApp(this) FirebaseMessaging.getInstance().token.addOnCompleteListener { task -> if (!task.isSuccessful) { Log.w(TAG, "Fetching FCM registration token failed", task.exception) return@addOnCompleteListener } val token = task.result Log.d(TAG, "FCM token received: $token") } } ``` ### Pass the token to the SDK Provide the FCM token when connecting the `TelnyxClient`. The SDK registers it with Telnyx so push notifications can be routed to this device. ```kotlin val credentialConfig = CredentialConfig( sipUser = username, sipPassword = password, fcmToken = fcmToken ) telnyxClient.connect( txPushMetaData = txPushMetaData, credentialConfig = credentialConfig, ) ``` ### Handle incoming push notifications Create a `FirebaseMessagingService` to process incoming FCM messages. Parse the `metadata` field from the notification payload and pass it to your notification UI: ```kotlin override fun onMessageReceived(remoteMessage: RemoteMessage) { super.onMessageReceived(remoteMessage) val params = remoteMessage.data val objects = JSONObject(params as Map<*, *>) val metadata = objects.getString("metadata") val isMissedCall = objects.getString("message") == "Missed call!" if (isMissedCall) { // Handle missed call — stop ringing, dismiss notification return } // Show incoming call notification with metadata showIncomingCallNotification(metadata) } ``` When the user answers, reconnect to the socket with the push metadata so the SDK can receive the pending invitation: ```kotlin telnyxClient.connect( txPushMetaData = txPushMetaData, credentialConfig = credentialConfig, ) ``` ### Decline calls from push notifications The SDK provides `connectWithDeclinePush()` to decline incoming calls without fully reconnecting: ```kotlin telnyxClient.connectWithDeclinePush( config = credentialConfig, txPushMetaData = txPushMetaData.toJson() ) ``` This connects briefly with a `decline_push: true` parameter, handles the decline, and disconnects automatically. ### Android 14 permissions Android 14 requires explicit notification permissions. Add these to your `AndroidManifest.xml`: ```xml ``` Request `POST_NOTIFICATIONS` at runtime before showing notifications. --- ## Troubleshooting ### FCM token not passed to login Verify that the FCM token is retrieved successfully and included in the `CredentialConfig` or `TokenConfig` passed to `connect()`. Check your logs for the token value. ### Incorrect google-services.json Confirm that the `google-services.json` file is in your app module's root directory and the package name matches your application. ### Wrong push credential on the SIP Connection In the Telnyx Portal, open your SIP Connection → **WebRTC** tab → **Android** section and verify the correct credential is selected. ### Invalid push credential If the service account JSON is malformed or from the wrong Firebase project, push delivery fails silently. Generate a fresh key from the Firebase Console and update the credential in the Portal. ### Testing push delivery The SDK repository includes a testing tool in the `push-notification-tool/` directory that sends test FCM notifications to verify your setup independently of the Telnyx call flow: ```bash cd push-notification-tool npm install npm start ``` If test notifications arrive but calls don't trigger pushes, the issue is in your Portal or SIP Connection configuration rather than Firebase. ## Next steps - [Push notifications overview](/docs/voice/webrtc/push-notifications) — Multidevice support and architecture - [Android SDK reference](/docs/development/webrtc/android-sdk) — Full SDK documentation - [Mobile Push Credentials API](/api/webrtc/mobile-push-credentials) — Manage credentials programmatically --- ### iOS > Source: https://developers.telnyx.com/docs/voice/webrtc/push-notifications/ios.md ## Prerequisites - A [Telnyx account](https://portal.telnyx.com) with a configured SIP Connection - An [Apple Developer account](https://developer.apple.com/) - The Telnyx iOS WebRTC SDK integrated into your application ## Portal setup ### 1. Create a VoIP push certificate For official Apple documentation, see [Create VoIP Services Certificates](https://developer.apple.com/help/account/certificates/create-voip-services-certificates). You will need: - An Apple Developer account - Your app's Bundle ID - A Certificate Signing Request (CSR) from your Mac **Generate the certificate:** 1. Go to [developer.apple.com](https://developer.apple.com/) and sign in. 2. Navigate to **Certificates, Identifiers & Profiles**. 3. Click the **+** button to create a new certificate. 4. Select **VoIP Services Certificate** and click **Continue**. 5. Choose the Bundle ID for your application and click **Continue**. 6. Upload a CSR file from your Mac. **Generate a CSR** (if you don't have one): 1. Open **Keychain Access** on your Mac. 2. Go to **Keychain Access → Certificate Assistant → Request a Certificate from a Certificate Authority**. 3. Enter your email address, select **Save to disk**, and click **Continue**. After uploading the CSR, download the generated certificate (usually named `voip_services.cer`) and double-click it to install it in your Keychain. A single VoIP Services Certificate works for both APNS sandbox and production environments. You need a separate certificate for each Bundle ID. ### 2. Export cert.pem and key.pem 1. Open **Keychain Access** and search for "VoIP Services". 2. Verify the certificate is installed for your Bundle ID. 3. Right-click the certificate and select **Export**. Save as a `.p12` file (you'll be prompted for a password). 4. Run the following commands to extract the PEM files: ```bash openssl pkcs12 -in PATH_TO_YOUR_P12 -nokeys -out cert.pem -nodes -legacy openssl pkcs12 -in PATH_TO_YOUR_P12 -nocerts -out key.pem -nodes -legacy openssl rsa -in key.pem -out key.pem ``` ### 3. Create an iOS push credential in the Telnyx Portal 1. Go to [portal.telnyx.com](https://portal.telnyx.com) and log in. 2. Navigate to **API Keys** in the left panel. 3. Select the **Credentials** tab, then click **Add → iOS Credential**. 4. Enter a credential name (using your Bundle ID makes it easy to identify). 5. Paste the full contents of `cert.pem` into the certificate field (include the `-----BEGIN CERTIFICATE-----` and `-----END CERTIFICATE-----` markers). 6. Paste the full contents of `key.pem` into the key field (include the `-----BEGIN RSA PRIVATE KEY-----` and `-----END RSA PRIVATE KEY-----` markers). 7. Click **Add Push Credential** to save. ### 4. Attach the credential to a SIP Connection 1. Navigate to **SIP Connections** in the left panel. 2. Open the SIP Connection you want to configure (or [create a new one](/docs/voice/sip-trunking/get-started)). 3. Select the **WebRTC** tab. 4. In the **iOS** section, select the push credential you created. 5. Save the SIP Connection. --- ## App setup ### Enable push notification capabilities 1. Open your Xcode project. 2. Select your app target in the Project Navigator. 3. Go to **Signing & Capabilities** and click **+ Capability**. 4. Add **Push Notifications**. 5. Add **Background Modes** and enable **Voice over IP**. ### Configure PushKit Import PushKit and register for VoIP push notifications: ```swift import PushKit private var pushRegistry = PKPushRegistry(queue: DispatchQueue.main) func initPushKit() { pushRegistry.delegate = self pushRegistry.desiredPushTypes = Set([.voIP]) } ``` Implement the `PKPushRegistryDelegate` to capture the device token: ```swift extension AppDelegate: PKPushRegistryDelegate { func pushRegistry(_ registry: PKPushRegistry, didUpdate credentials: PKPushCredentials, for type: PKPushType) { if type == .voIP { let deviceToken = credentials.token.map { String(format: "%02X", $0) }.joined() // Store this token — you'll pass it to TelnyxClient on login } } func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) { if payload.type == .voIP { handleVoIPPushNotification(payload: payload) } completion() } } ``` ### Pass the token to the SDK Include the APNS device token when connecting the `TelnyxClient`: ```swift let txConfig = TxConfig( sipUser: sipUser, password: password, pushDeviceToken: "DEVICE_APNS_TOKEN", logLevel: .all ) ``` Or with a JWT token: ```swift let txConfig = TxConfig( token: "MY_JWT_TELNYX_TOKEN", pushDeviceToken: "DEVICE_APNS_TOKEN", logLevel: .all ) ``` ### Handle incoming VoIP push notifications When a push notification arrives, reconnect the client and report the call to CallKit: ```swift func handleVoIPPushNotification(payload: PKPushPayload) { guard let metadata = payload.dictionaryPayload["metadata"] as? [String: Any] else { return } let callerName = (metadata["caller_name"] as? String) ?? "" let callerNumber = (metadata["caller_number"] as? String) ?? "" let caller = callerName.isEmpty ? (callerNumber.isEmpty ? "Unknown" : callerNumber) : callerName let txConfig = TxConfig( sipUser: sipUser, password: password, pushDeviceToken: "APNS_PUSH_TOKEN" ) try? telnyxClient?.processVoIPNotification( txConfig: txConfig, serverConfiguration: serverConfig, pushMetaData: metadata ) // Report incoming call to CallKit let callHandle = CXHandle(type: .generic, value: caller) let callUpdate = CXCallUpdate() callUpdate.remoteHandle = callHandle callUpdate.hasVideo = false if let callId = metadata["call_id"] as? String, let uuid = UUID(uuidString: callId) { provider.reportNewIncomingCall(with: uuid, update: callUpdate) { error in if let error = error { print("Failed to report incoming call: \(error.localizedDescription)") } } } } ``` On iOS 13.0 and later, you **must** report incoming VoIP push notifications to CallKit. If you fail to do so, the system will terminate your app. See [Apple's documentation](https://developer.apple.com/documentation/pushkit/pkpushregistrydelegate/2875784-pushregistry) for details. ### Disable push notifications To disable push notifications for the current user: ```swift telnyxClient.disablePushNotifications() ``` Signing back in with the same credentials re-enables push notifications. --- ## Troubleshooting ### VoIP certificate issues - Verify your VoIP Services Certificate is not expired. - Ensure the certificate matches the Bundle ID used in your app. - For different Bundle IDs (e.g., `com.myapp.dev` vs `com.myapp`), create separate certificates. ### Push token not passed to login Check that the APNS device token is captured in `pushRegistry(_:didUpdate:for:)` and included in `TxConfig` when calling `connect()`. ### Wrong credential on the SIP Connection In the Telnyx Portal, open your SIP Connection → **WebRTC** tab → **iOS** section and verify the correct credential is selected. ### APNS environment mismatch - **Debug builds** (Xcode): Use sandbox environment — set `pushEnvironment` to `sandbox` in `TxConfig`. - **Release builds / TestFlight**: Use production environment — set `pushEnvironment` to `production`. - Ensure the APNS environment matches your build signing profile. ### Testing push delivery The SDK repository includes a testing tool in the `push-notification-tool/` directory: ```bash cd push-notification-tool npm install npm run dev ``` You'll need your device token, Bundle ID, `cert.pem`, `key.pem`, and the target APNS environment (sandbox or production). Common error responses from the tool: - **BadDeviceToken**: Token is invalid or expired - **BadCertificate**: Certificate files are invalid or expired - **BadTopic**: Bundle ID doesn't match certificate - **TopicDisallowed**: Certificate doesn't have VoIP permissions ## Next steps - [Push notifications overview](/docs/voice/webrtc/push-notifications) — Multidevice support and architecture - [iOS SDK reference](/docs/development/webrtc/ios-sdk) — Full SDK documentation - [Mobile Push Credentials API](/api/webrtc/mobile-push-credentials) — Manage credentials programmatically --- ### Flutter > Source: https://developers.telnyx.com/docs/voice/webrtc/push-notifications/flutter.md ## Prerequisites - A [Telnyx account](https://portal.telnyx.com) with a configured SIP Connection - The Telnyx Flutter Voice SDK integrated into your application - **Android**: A [Firebase project](https://console.firebase.google.com/) with Cloud Messaging enabled - **iOS**: An [Apple Developer account](https://developer.apple.com/) with a VoIP push certificate ## Portal setup Flutter apps are cross-platform, so you need credentials for each platform you target: - **Android**: Follow the [Android portal setup](/docs/voice/webrtc/push-notifications/android#portal-setup) to create an Android push credential using your Firebase service account JSON. - **iOS**: Follow the [iOS portal setup](/docs/voice/webrtc/push-notifications/ios#portal-setup) to create an iOS push credential using your VoIP certificate PEM files. Attach both credentials to your SIP Connection under the **WebRTC** tab in the Telnyx Portal. --- ## App setup ### Android — Firebase Cloud Messaging #### 1. Listen for background push notifications Register a background message handler in your `main` method: ```dart @pragma('vm:entry-point') Future main() async { WidgetsFlutterBinding.ensureInitialized(); if (defaultTargetPlatform == TargetPlatform.android) { await Firebase.initializeApp(); FirebaseMessaging.onBackgroundMessage( _firebaseMessagingBackgroundHandler, ); await FirebaseMessaging.instance .setForegroundNotificationPresentationOptions( alert: true, badge: true, sound: true, ); } runApp(const MyApp()); } ``` #### 2. Handle the push notification Process the incoming message and show a call notification using a plugin like [FlutterCallkitIncoming](https://pub.dev/packages/flutter_callkit_incoming): ```dart Future _firebaseMessagingBackgroundHandler( RemoteMessage message, ) async { // Show incoming call notification CallKitParams callKitParams = CallKitParams( android: ..., ios: ..., extra: message.data, ); await FlutterCallkitIncoming.showCallkitIncoming(callKitParams); // Listen for user action FlutterCallkitIncoming.onEvent.listen((CallEvent? event) async { switch (event!.event) { case Event.actionCallAccept: TelnyxClient.setPushMetaData( message.data, isAnswer: true, isDecline: false, ); break; case Event.actionCallDecline: TelnyxClient.setPushMetaData( message.data, isAnswer: false, isDecline: true, ); break; } }); } ``` #### 3. Create a high-importance notification channel (Android 8.0+) For Android 8.0 and higher, create a dedicated notification channel so incoming call notifications display as heads-up alerts. Use the [flutter_local_notifications](https://pub.dev/packages/flutter_local_notifications) package to configure the channel with maximum importance. ### iOS — Apple Push Notification Service For iOS, the Flutter SDK uses APNS through the native PushKit integration. Configure your iOS project following the standard [iOS app setup](/docs/voice/webrtc/push-notifications/ios#app-setup), which includes: 1. Enabling Push Notifications and Background Modes (VoIP) capabilities in Xcode. 2. Configuring PushKit to register for VoIP pushes. 3. Reporting incoming calls to CallKit (required on iOS 13+). The Flutter SDK handles the bridge between native push events and your Dart code. --- ## Troubleshooting ### Android-specific issues - **FCM token not received**: Ensure `Firebase.initializeApp()` is called before requesting the token and that `google-services.json` is correctly placed. - **Notifications not showing in background**: Verify your background handler is annotated with `@pragma('vm:entry-point')` and registered via `FirebaseMessaging.onBackgroundMessage`. - **Low-priority notifications**: Create a notification channel with `Importance.max` for incoming call alerts. ### iOS-specific issues - **No push notifications**: Confirm the VoIP push certificate matches your Bundle ID and is uploaded to the Telnyx Portal. - **App terminated on push**: On iOS 13+, you must report every VoIP push to CallKit or the system kills your app. - **Environment mismatch**: Use sandbox for debug builds and production for release/TestFlight builds. ### General - **Push works but no call invitation**: The push notification only signals that a call is incoming. Your app must reconnect to the TelnyxClient socket after receiving the push so the actual invitation can be delivered. - **Multidevice**: A user can register up to 5 push tokens. If a 6th is added, the oldest is removed. ## Next steps - [Push notifications overview](/docs/voice/webrtc/push-notifications) — Multidevice support and architecture - [Flutter SDK reference](/docs/development/webrtc/flutter-sdk) — Full SDK documentation - [Mobile Push Credentials API](/api/webrtc/mobile-push-credentials) — Manage credentials programmatically --- ### React Native > Source: https://developers.telnyx.com/docs/voice/webrtc/push-notifications/react-native.md ## Prerequisites - A [Telnyx account](https://portal.telnyx.com) with a configured SIP Connection - The `@telnyx/react-voice-commons-sdk` integrated into your application - **Android**: A [Firebase project](https://console.firebase.google.com/) with Cloud Messaging enabled - **iOS**: An [Apple Developer account](https://developer.apple.com/) with a VoIP push certificate ## Portal setup React Native apps are cross-platform, so you need credentials for each platform you target: - **Android**: Follow the [Android portal setup](/docs/voice/webrtc/push-notifications/android#portal-setup) to create an Android push credential using your Firebase service account JSON. - **iOS**: Follow the [iOS portal setup](/docs/voice/webrtc/push-notifications/ios#portal-setup) to create an iOS push credential using your VoIP certificate PEM files. Attach both credentials to your SIP Connection under the **WebRTC** tab in the Telnyx Portal. --- ## App setup ### Install dependencies ```bash # iOS VoIP push notifications npm install react-native-voip-push-notification # Expo notifications for Android FCM token (if using Expo) npx expo install expo-notifications ``` ### Android — Firebase Cloud Messaging #### 1. Add the Firebase configuration file Download `google-services.json` from your Firebase project console and place it in your project root (same level as `package.json`): ``` your-project/ ├── google-services.json ├── package.json ├── android/ └── ios/ ``` #### 2. Configure the Android manifest Add the Firebase messaging service and Telnyx notification receiver to `android/app/src/main/AndroidManifest.xml`: ```xml ``` #### 3. Retrieve the FCM token The SDK handles FCM token retrieval internally on Android. Pass the token to the SDK when connecting: ```typescript import { TelnyxVoIPClient } from '@telnyx/react-voice-commons-sdk'; const client = new TelnyxVoIPClient({ credentialConfig: { sipUser: 'username', sipPassword: 'password', }, }); ``` ### iOS — Apple Push Notification Service #### 1. Configure PushKit Use the `react-native-voip-push-notification` package to register for VoIP pushes and capture the device token: ```typescript import VoipPushNotification from 'react-native-voip-push-notification'; VoipPushNotification.addEventListener('register', (token: string) => { // Store this token — pass it to the SDK on login console.log('VoIP push token:', token); }); VoipPushNotification.addEventListener( 'notification', (notification: any) => { // Handle incoming VoIP push notification const metadata = notification.metadata; // Process the call... }, ); VoipPushNotification.registerVoipToken(); ``` #### 2. Enable capabilities in Xcode 1. Open your iOS project in Xcode. 2. Go to **Signing & Capabilities**. 3. Add **Push Notifications**. 4. Add **Background Modes** and enable **Voice over IP**. On iOS 13.0 and later, you **must** report incoming VoIP push notifications to CallKit. If you fail to do so, the system will terminate your app. --- ## Troubleshooting ### Android-specific issues - **FCM token not received**: Verify `google-services.json` is in the correct location and the package name matches your app. - **No notifications in background**: Ensure the Firebase messaging service is declared in your Android manifest. - **Wrong credential on SIP Connection**: Check the Telnyx Portal → SIP Connection → WebRTC → Android section. ### iOS-specific issues - **No push notifications**: Confirm the VoIP push certificate matches your Bundle ID and is uploaded to the Telnyx Portal. - **App terminated on push**: Report every VoIP push to CallKit on iOS 13+. - **Environment mismatch**: Use sandbox for debug builds and production for release/TestFlight. ### General - **Push works but no call invitation**: The push notification signals an incoming call. Your app must reconnect to the socket after receiving the push so the SDK can receive the actual invitation. - **Multidevice**: A user can register up to 5 push tokens across iOS and Android devices. ## Next steps - [Push notifications overview](/docs/voice/webrtc/push-notifications) — Multidevice support and architecture - [React Native SDK reference](/docs/development/webrtc/react-native-sdk) — Full SDK documentation - [Mobile Push Credentials API](/api/webrtc/mobile-push-credentials) — Manage credentials programmatically --- ## Tutorials ### JS SDK Demo App > Source: https://developers.telnyx.com/docs/voice/webrtc/js-sdk/demo-app.md To lower onboarding barrier, a JS SDK demo app was built and made accessible at [webrtc.telnyx.com](https://webrtc.telnyx.com). To use it, complete the following procedure. Instead of portal.telnyx.com screenshots being displayed, only API requests are presented, as frequent UI improvements render this page out of date. ## Pre-req 1: Account Balance Sign up and top up the account with a small amount of credit, e.g. $5. ## Pre-req 2: Outbound Voice Profile (OVP) ```json POST /v2/outbound_voice_profiles HTTP/1.1 Host: api.telnyx.com Content-Type: application/json Authorization: Bearer XXX Content-Length: 78 { "name": "webrtc", "whitelisted_destinations": [ "US" ] } ``` ## Pre-req 3: Credential Based SIP Connection ```json POST /v2/credential_connections HTTP/1.1 Host: api.telnyx.com Content-Type: application/json Authorization: Bearer XXX Content-Length: 288 { "active": true, "password": "xxx", "user_name": "xxx", "anchorsite_override": "Latency", "connection_name": "sample-connection", "sip_uri_calling_preference": null, "outbound": { "outbound_voice_profile_id": "2532742229592638840" } } ``` where the `outbound_voice_profile_id` is the `id` returned in the previous API request. ## Pre-req 4: Phone Number For ease of activation, choose US or CA phone numbers as there exists no regulatory requirements for their immediate use. ```json GET /v2/available_phone_numbers?filter[country_code]=US HTTP/1.1 Host: api.telnyx.com Authorization: Bearer XXX ``` In the response, choose a phone number. Place an order with the desired phone number and the `connection_id` from the previous step. ```json POST /v2/number_orders HTTP/1.1 Host: api.telnyx.com Content-Type: application/json Authorization: Bearer XXX Content-Length: 119 { "phone_numbers": [ { "phone_number": "+18669236951" } ], "connection_id": "2532747013766776351" } ``` The order will be `pending` in the immediate response. After a short wait, poll the order status. ```json GET /v2/number_orders/3d8bd753-2162-4ce2-bc5e-96b5cad7fedb HTTP/1.1 Host: api.telnyx.com Authorization: Bearer XXX ``` Ensure the `status` is `success` before proceeding. ```json { "data": { "updated_at": "2024-10-02T12:42:01.637193+00:00", "created_at": "2024-10-02T12:42:01.637193+00:00", "requirements_met": true, "messaging_profile_id": null, "customer_reference": null, "phone_numbers": [ { "requirements_status": "approved", "requirements_met": true, "phone_number": "+18669236951", "country_code": "US", "bundle_id": null, "id": "be663ad6-e9c2-4943-a6fa-0bfaddccaae1", "regulatory_requirements": [], "phone_number_type": "toll_free", "status": "success", "record_type": "number_order_phone_number" } ], "connection_id": "2532747013766776351", "phone_numbers_count": 1, "billing_group_id": null, "id": "3d8bd753-2162-4ce2-bc5e-96b5cad7fedb", "sub_number_orders_ids": [ "407cae20-03af-4b0d-a613-fdfb241d4bc1" ], "status": "success", "record_type": "number_order" } } ``` ## Setting Up and Using the Demo App Follow [this instruction](https://developers.telnyx.com/docs/voice/webrtc/auth/telephony-credentials) to create a telephony credential. The demo app should have the following configuration * “Authentication” → “Credential” * “SIP Username” → from telephony credential * “Password” → from telephony credential * “Caller ID Name” → purchased phone number in +E164 format * “Caller ID Number” → purchased phone number in +E164 format After clicking “Connect”, you should see `registered` in the log to the right. ## Making Call To make an outbound call, put the destination phone number in +E164 format. Ensure the destination country is in the `whitelisted_destinations` of the configured OVP. ## Receiving Call Open another tab and successfully register another client. From that client, dial `[xxx]@sip.telnyx.com` where `xxx` is the `sip_username` of the telephony credential of the first client. It starts with `gencred`. ![](/assets/images/webrtc-demo.png) Alternatively, register this client with the credentials of the SIP connection created earlier. You may dial the phone number directly from your mobile device. See [Dialing Registered Clients](https://developers.telnyx.com/docs/voice/webrtc/sdk-commonalities#dialing-registered-clients) for more detail. ## Additional Resources * [Anatomy of the JS SDK](https://developers.telnyx.com/docs/voice/webrtc/js-sdk/anatomy#overview). * [OVP API Reference](/api-reference/outbound-voice-profiles/create-an-outbound-voice-profile) * [Credential Based Connection API Reference](https://developers.telnyx.com/api-reference/credential-connections/create-a-credential-connection#create-a-credential-connection) * [Number Searching API Reference](/api-reference/phone-number-search/list-available-phone-numbers) * [Number Order API Reference](/api-reference/phone-number-orders/create-a-number-order) --- ### JS SDK Anatomy > Source: https://developers.telnyx.com/docs/voice/webrtc/js-sdk/anatomy.md While some differences exist between the JS SDK and the mobile SDKs, they follow a similar client lifecycle and call flow. The JS SDK demo app is used here as it’s far easier to set up the application (just load up webrtc.telnyx.com) and perform debugging using browser tooling. ## Overview The SDK does two main things: * Establishes an active websocket connection to send and receive signaling messages to and from rtc.telnyx.com. * Establishes a media session for a call To achieve the above, it employs the following suite of APIs: * [WebSocket API](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) * [WebRTC API](https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API) * [Media Capture and Streams API](https://developer.mozilla.org/en-US/docs/Web/API/Media_Capture_and_Streams_API) ## Client Instantiation & Authentication 1. Go to webrtc.telnyx.com 2. Right click; Inspect; Select Network tab and filter WS traffic 3. Follow [this page](https://developers.telnyx.com/docs/voice/webrtc/js-sdk/demo-app) to successfully register the demo app. ![](/assets/images/demo-debug.png) In the browser, the following sequence of JSON-RPC messages is observed. Message 1: client → rtc.telnyx.com ```json { "jsonrpc": "2.0", "id": "2c754d41-b7e6-422d-b39f-661a139cd5b3", "method": "login", "params": { "login": "xxx", "passwd": "yyy", "userVariables": {}, "loginParams": {}, "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36", "sessid": "cf7894a7-c225-428a-a8ae-4145833e6ddb" } } ``` Message 2: rtc.telnyx.com → client ```json { "id": "2c754d41-b7e6-422d-b39f-661a139cd5b3", "jsonrpc": "2.0", "result": { "message": "logged in", "sessid": "cf7894a7-c225-428a-a8ae-4145833e6ddb" }, "voice_sdk_id": "VSDK1Ch8eUTpaTTKMC3HTSjybKJ3apyGYgw" } ``` Message 3: rtc.telnyx.com → client ```json { "id": 138417, "jsonrpc": "2.0", "method": "telnyx_rtc.clientReady", "params": { "reattached_sessions": [] }, "voice_sdk_id": "VSDK1Ch8eUTpaTTKMC3HTSjybKJ3apyGYgw" } ``` Message 4: client → rtc.telnyx.com ```json { "jsonrpc": "2.0", "id": "660360fb-3f46-4f63-804b-973e429c7c22", "method": "telnyx_rtc.gatewayState", "params": {} } ``` Message 5: rtc.telnyx.com → client ```json { "id": "660360fb-3f46-4f63-804b-973e429c7c22", "jsonrpc": "2.0", "result": { "params": { "state": "REGED" }, "sessid": "cf7894a7-c225-428a-a8ae-4145833e6ddb" }, "voice_sdk_id": "VSDK1Ch8eUTpaTTKMC3HTSjybKJ3apyGYgw" } ``` The above interaction is caused by the demo app instantiating and connecting the SDK client. ```javascript const client = new TelnyxRTC({login: "xxx", password: "yyy"}); client.connect(); ``` At a high level, when the client is instantiated, it… * creates a [session object](https://github.com/team-telnyx/webrtc/blob/main/packages/js/src/Modules/Verto/BaseSession.ts#L42) and * registers handlers for all [4 socket events](https://github.com/team-telnyx/webrtc/blob/main/packages/js/src/Modules/Verto/BaseSession.ts#L335) Invoking the `connect` method on the SDK client … * Initiates a WebSocket connection to rtc.telnyx.com * Once the socket is open, the [login message](https://github.com/team-telnyx/webrtc/blob/main/packages/js/src/Modules/Verto/index.ts#L63) is sent to rtc.telnyx.com. At this point, Message #1 and #2 are observed. Message #3, #4, and #5, is the result of the logic [here](https://github.com/team-telnyx/webrtc/blob/main/packages/js/src/Modules/Verto/webrtc/VertoHandler.ts#L120) * A `telnyx_rtc.clientReady` event from rtc.telnyx.com triggers a `telnyx_rtc.gateState` query from the SDK client * A `REGED` event from rtc.telnyx.com bubbles up as [`telnyx.ready`](https://github.com/team-telnyx/webrtc/blob/main/packages/js/docs/ts/classes/TelnyxRTC.md#events). At this point, the SDK client is authenticated to make or receive a call. ## Call Initiation 1. In another tab, open chrome://webrtc-internals/ 2. Fill in “Call destination” with +18008648331 (United Airlines IVR) 3. Click “Call” In the browser, the following sequence of JSON-RPC messages is observed. Message 1: client → rtc.telnyx.com ```json { "jsonrpc":"2.0", "id":"71344c55-10a9-4f6a-b8a8-ebaa9347bc95", "method":"telnyx_rtc.invite", "params":{ "sessid":"cf7894a7-c225-428a-a8ae-4145833e6ddb", "sdp":"[ABRIDGED SDP]", "dialogParams":{ "audio":true, "useStereo":false, "debug":false, "debugOutput":"socket", "attach":false, "screenShare":false, "userVariables":{ "microphoneLabel":"Default - AirPods" }, "mediaSettings":{ }, "iceServers":[ { "urls":"turn:turn.telnyx.com:3478?transport=tcp", "username":"testuser", "credential":"testpassword" }, { "urls":"stun:stun.telnyx.com:3478" }, { "urls":[ "stun:stun.l.google.com:19302" ] } ], "localElement":"localVideo", "remoteElement":"remoteVideo", "ringtoneFile":"https://webrtc.telnyx.com/sounds/incoming_call.mp3", "stats":true, "callID":"074e7e59-6859-4b8b-8743-83df82e7f776", "destination_number":"+18008648331", "remote_caller_id_name":"Outbound Call", "remote_caller_id_number":"+18008648331", "caller_id_name":"+15734038245", "caller_id_number":"XXX" }, "User-Agent":"Web-2.16.0" } } ``` Message 2: rtc.telnyx.com → client ```json { "id": "71344c55-10a9-4f6a-b8a8-ebaa9347bc95", "jsonrpc": "2.0", "result": { "callID": "074e7e59-6859-4b8b-8743-83df82e7f776", "message": "CALL CREATED", "sessid": "cf7894a7-c225-428a-a8ae-4145833e6ddb" }, "voice_sdk_id": "VSDK1CiEGUTpa63GGtH2nSmyTHM7KIwlL_w" } ``` Message 3: rtc.telnyx.com → client ```json { "id": 254271, "jsonrpc": "2.0", "method": "telnyx_rtc.ringing", "params": { "callID": "074e7e59-6859-4b8b-8743-83df82e7f776", "callee_id_name": "Outbound Call", "callee_id_number": "+18008648331", "caller_id_name": "+15734038245", "caller_id_number": "XXX", "dialogParams": { "custom_headers": [] }, "display_direction": "inbound", "telnyx_leg_id": "b6a23a2c-7e12-11ef-ab96-02420aef821f", "telnyx_session_id": "b6a23ea0-7e12-11ef-a5d3-02420aef821f" }, "voice_sdk_id": "VSDK1CiEGUTpa63GGtH2nSmyTHM7KIwlL_w" } ``` Message 4: client → rtc.telnyx.com ```json { "jsonrpc": "2.0", "id": 254271, "result": { "method": "telnyx_rtc.ringing" } } ``` Message 5: rtc.telnyx.com → client ```json { "id": 254274, "jsonrpc": "2.0", "method": "telnyx_rtc.media", "params": { "callID": "074e7e59-6859-4b8b-8743-83df82e7f776", "dialogParams": { "custom_headers": [] }, "sdp": "[ABRIDGED SDP]", "variables": { "Core-UUID": "be37d1d2-14e2-45de-af9f-0b2807445761", "Event-Calling-File": "switch_channel.c", "Event-Calling-Function": "switch_channel_get_variables_prefix", "Event-Calling-Line-Number": "4632", "Event-Date-GMT": "Sun, 29 Sep 2024 03:27:13 GMT", "Event-Date-Local": "2024-09-29 03:27:13", "Event-Date-Timestamp": "1727580433259250", "Event-Name": "CHANNEL_DATA", "Event-Sequence": "1071399", "FreeSWITCH-Hostname": "b2bua-rtc-canary.tel-sy1-ibm-prod-413", "FreeSWITCH-IPv4": "10.33.6.81", "FreeSWITCH-IPv6": "::1", "FreeSWITCH-Switchname": "b2bua-rtc-canary.tel-sy1-ibm-prod-413" } }, "voice_sdk_id": "VSDK1CiEGUTpa63GGtH2nSmyTHM7KIwlL_w" } ``` Message 6: client → rtc.telnyx.com ```json { "jsonrpc": "2.0", "id": 254274, "result": { "method": "telnyx_rtc.media" } } ``` Message 7: rtc.telnyx.com → client ```json { "id": 254275, "jsonrpc": "2.0", "method": "telnyx_rtc.answer", "params": { "callID": "074e7e59-6859-4b8b-8743-83df82e7f776", "dialogParams": { "custom_headers": [] }, "variables": { "Core-UUID": "be37d1d2-14e2-45de-af9f-0b2807445761", "Event-Calling-File": "switch_channel.c", "Event-Calling-Function": "switch_channel_get_variables_prefix", "Event-Calling-Line-Number": "4632", "Event-Date-GMT": "Sun, 29 Sep 2024 03:27:15 GMT", "Event-Date-Local": "2024-09-29 03:27:15", "Event-Date-Timestamp": "1727580435119306", "Event-Name": "CHANNEL_DATA", "Event-Sequence": "1071412", "FreeSWITCH-Hostname": "b2bua-rtc-canary.tel-sy1-ibm-prod-413", "FreeSWITCH-IPv4": "10.33.6.81", "FreeSWITCH-IPv6": "::1", "FreeSWITCH-Switchname": "b2bua-rtc-canary.tel-sy1-ibm-prod-413" } }, "voice_sdk_id": "VSDK1CiEGUTpa63GGtH2nSmyTHM7KIwlL_w" } ``` Message 8: client → rtc.telnyx.com ```json { "jsonrpc": "2.0", "id": 254275, "result": { "method": "telnyx_rtc.answer" } } ``` At this point, the media will flow. All of the above interaction is the result of the following SDK API call. ```javascript client.newCall(options); ``` Under the hood, the SDK performs many steps before Message #1 (INVITE) is even sent. ![](/assets/images/webrtc-internals.png) Broadly speaking, the following are the essential steps with the relevant data picked out from the JSON dump. 1. [`RTCPeerConnection`](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection) is instantiated. 2. [`getUserMedia`](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia) is invoked to obtain user’s permission for audio and eventually the [`MediaStream`](https://developer.mozilla.org/en-US/docs/Web/API/MediaStream) ```json { "audio_track_info": "id:6bd2b2da-d615-4ef3-9ac2-394abb22d6b0 label:Default - AirPods", "pid": 44341, "request_id": 29, "request_type": "getUserMedia", "rid": 303, "stream_id": "7cc701ad-3a35-4c2a-9b1e-c4c7b209f30c", "timestamp": 1727582437707.941 } ``` 3. [`addTransceiver`](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/addTransceiver) is invoked to add the local stream to the sender of the `RTCPeerConnection`. ```json { "time": "9/28/2024, 11:00:37 PM", "type": "transceiverAdded", "value": "Caused by: addTransceiver\n\ngetTransceivers()[0]:{\n mid:null,\n kind:'audio',\n sender:{\n track:'6bd2b2da-d615-4ef3-9ac2-394abb22d6b0',\n streams:['7cc701ad-3a35-4c2a-9b1e-c4c7b209f30c'],\n encodings: [\n {active: true, },\n ],\n },\n receiver:{\n track:'434cac64-3afe-4705-bfe7-979d9530b58e',\n streams:[],\n },\n direction:'sendrecv',\n currentDirection:null,\n}" } ``` 4. The previous step triggers the [`negotiationneeded`](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/negotiationneeded_event) event. ```json { "time": "9/28/2024, 11:00:37 PM", "type": "negotiationneeded", "value": "" } ``` 5. In the event handler, the SDK invokes [`createOffer`](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/createOffer). ```json { "time": "9/28/2024, 11:00:37 PM", "type": "createOffer", "value": "options: {offerToReceiveVideo: 0, offerToReceiveAudio: 1, voiceActivityDetection: true, iceRestart: false}" } ``` 6. This API call will eventually create a [`RTCSessionDescription`](https://developer.mozilla.org/en-US/docs/Web/API/RTCSessionDescription) with information on the local media stream. ```json { "time": "9/28/2024, 11:00:37 PM", "type": "createOfferOnSuccess", "value": "type: offer, sdp: [ABRIDGED SDP]" } ``` 7. The SDK will then invoke the [`setLocalDescription`](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/setLocalDescription) to set the SDP of the client peer. ```json { "time": "9/28/2024, 11:00:37 PM", "type": "setLocalDescription", "value": "type: offer, sdp: [ABRIDGED SDP]" } ``` 8. Concurrently, `createOffer` also kicks off the ICE candidate gathering. ```json { "time": "9/28/2024, 11:00:37 PM", "type": "icegatheringstatechange", "value": "gathering" }, ..., { "time": "9/28/2024, 11:00:37 PM", "type": "icecandidate", "value": "sdpMid: 0, sdpMLineIndex: 0, candidate: candidate:134647514 1 udp 1685855999 18.163.7.125 59374 typ srflx raddr 100.113.237.26 rport 59374 generation 0 ufrag ccb7 network-id 3 network-cost 50, url: stun:stun.l.google.com:19302" } ``` 9. When all is done, `icecandidate` event triggers with `candidate = null` to indicate the process is completed. Subsequently, the existing local SDP parameters are augmented with the ICE candidates. 10. At this point, all necessary info are present to send the invite to rtc.telnyx.com. 11. As a result, on the websocket, Message #1 through #4 are observed. 12. At Message #5, rtc.telnyx.com sends over its SDP in the `telnyx_rtc.media` events. 13. Upon receipt of this message, the SDK invokes [`setRemoteDescription`](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/setRemoteDescription) to set the SDP of the remote peer. ```json { "time": "9/28/2024, 11:00:44 PM", "type": "setRemoteDescription", "value": "type: answer, sdp: [ABRIDGED SDP]" } ``` 14. Finally, two peers of the `RTCPeerConnection` are fully identified. [connectionState](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/connectionState) changes from connecting to connected. ```json { "time": "9/28/2024, 11:00:44 PM", "type": "connectionstatechange", "value": "connecting" }, ... { "time": "9/28/2024, 11:00:45 PM", "type": "connectionstatechange", "value": "connected" } ``` 15. Media will flow over UDP. --- ## Use Cases ### Contact Center (CCaaS) > Source: https://developers.telnyx.com/docs/voice/webrtc/use-cases/contact-center.md ## Overview In building a Contact Center as a Service solution leveraging Telnyx WebRTC, enable SIP connection credentials with Park Outbound Calls and webhook events for enhanced functionality and seamless communication flows. ### Key features **Webhook events** - Monitor SIP connection events in real-time. - Receive notifications for call events: dialing, answering, bridging, hang-up, voicemail completion. - Primary/failover URL configuration for reliability. **Park Outbound Calls** - Temporarily hold calls until further instructions via Voice API. - Enable additional processing or decision-making before connecting. - Provide customizable call handling experiences. **Backend application requirements** - Utilize Telnyx's call control capabilities (Voice API documentation). - Issue commands based on webhook events: answer, play audio, bridge, transfer. - Handle sophisticated workflows for call routing. ### Inbound call flow 1. User calls main number, answered with text-to-speech greeting. 2. IVR menu presents options to mark call attributes (language, skills, department). 3. Call transferred to queue, parked while waiting for agent. 4. Auto-transfer to most idle agent or manual cherry-picking. 5. Call recording initiated when agent answers. 6. Call forwarded to multiple agents simultaneously with recording enabled. 7. Additional call control: mute, hold, transcription, text-to-speech announcements. ## Frontend implementation ### Authentication Agent desktop applications should have an authentication process implemented. We recommend using authentication tokens generated from individual telephony credentials created for each agent. When an agent logs in, the frontend app requests an authentication token from the backend, which is then used for subsequent API requests in the WebRTC client. When a call is received, you can see which agents are logged in with the on-demand generated credentials. Your call center service would use our Call Control API to dial each of the generated credentials to connect the caller with one of the available agents. Once agents are logged in, make sure your WebRTC client informs your call center backend that the agents are registered. This ensures the backend has a list of agents it can dial each time an inbound call is received to the main number. See more details in the [User authentication](#user-authentication) section in the Backend implementation for the Voice API methods to be used on the backend side. ### Agent desktop application Agent desktop application should support the following options: **Agent status management**: The agent should be able to report their current status, such as Available or Unavailable, so the backend application can see the currently available agents and decide which agent should receive the next call. Here is an example softphone application (WebRTC client) with an option to choose a preferred audio device. ![Agent desktop application with status management](/assets/images/agent-desktop-application.png) **Call control toolbar**: The toolbar is a set of buttons for handling calls, with options like Pickup, Disconnect, Mute, Hold, etc. ![Call control toolbar with call handling options](/assets/images/call-control-toolbar.png) **Queue view** allows you to monitor the calls parked in the queues and pick up a call manually. You can also present additional data like a position in a queue and estimated wait time. ![Queue view showing parked calls](/assets/images/queue-view.png) Here are the functions which would be used to build the above options in the frontend app: ### Audio device settings Get a list of available audio devices: ```javascript async function() { const client = new TelnyxRTC(options); let result = await client.getDevices(); console.log(result); } ``` Set active audio device: ```javascript const constraints = await client.setAudioSettings({ micId: '772e94959e12e589b1cc71133d32edf543d3315cfd1d0a4076a60601d4ff4df8', micLabel: 'Internal Microphone (Built-in)', echoCancellation: false }) ``` ### Call control toolbar Toggle microphone: ```javascript await call.toggleAudioMute() console.log(call.state) // => 'muted' await call.toggleAudioMute() console.log(call.state) // => 'unmuted' ``` Toggle call hold: ```javascript await call.toggleHold() console.log(call.state) // => 'held' await call.toggleHold() console.log(call.state) // => 'active' ``` ## Backend implementation The backend application handles call routing, IVR logic, and agent management through Telnyx Voice API webhooks. ### User authentication For each user, generate on-demand telephony credentials which should be stored in a database and associated with the user login. Agent desktop application should request an authentication token to be created based on the telephony credentials. **Generate on-demand telephony credentials** On-Demand Credentials help you onboard new customers or team members under your SIP connection, allowing you to separate each user with their own security credentials. This solution is ideal for integrating WebRTC into your own platforms, enabling your backend system to create outbound calls to each on-demand generated credential. You can use the optional parameter `expires_at` if you would like to set an expiration time for the credentials. ```javascript const telnyx = require('telnyx')('YOUR_API_KEY'); const { data: telephonyCredentials } = await telnyx.telephonyCredentials.create({ "connection_id": "1234567890", "name": "My-new-credential", "expires_at": EXPIRATION_DATE }); ``` **Create authentication token** ```javascript const telnyx = require('telnyx')('YOUR_API_KEY'); const accessToken = await telnyx.telephonyCredentials.generateAccessTokenFromCredential('CREDENTIAL_ID'); ``` ### Call flow In the backend application, we can fully control the call flow from the initiation of the call up to the call disconnect event. Based on the webhook notification, we can decide what kind of actions should be applied to the call. To monitor the call and proceed with the call flow, we should monitor call event types received on the webhook URL. Having an integration with the CRM application, we can retrieve caller data, for instance based on the caller number: ```javascript app.post("/api/voice/inbound", async (req, res) => { const { event_type } = req.body.data; const { payload } = req.body.data; const callData = await telnyx.calls.retrieve(payload.call_control_id); const isAlive = callData.data.is_alive; switch (event_type) { case "call.initiated": if (payload.direction === "incoming") { userObj = await get_caller_data({ voiceNumber: payload.to }); } else userObj = await get_caller_data({ voiceNumber: payload.from }); call_initiated(req, userObj); break; case "call.answered": call_answered(req, userObj); break; case "call.dtmf.received": call_dtmf_received(req, userObj); break; case "call.bridged": call_bridged(req, userObj); break; case "call.hangup": call_hangup(req, userObj); break; case "call.recording.saved": call_recording_saved(req, userObj); break; case "call.enqueued": call_enqueued(req, userObj); break; case "call.dequeued": call_dequeued(req, userObj); break; case "call.transcription": handleTranscription(payload, userObj); break; default: } return res.status(200).send({}); }); ``` For the `call.initiated` webhook, you should answer the call and provide an initial greeting with IVR options using the speak option: ```javascript const call_initiated = async (req) => { const { payload } = req.body.data; const call = new telnyx.Call({ call_control_id: payload.call_control_id, }); console.log(`Call initiated: ${payload.call_control_id}`); try { await call.answer(); console.log("Call answered:", payload.call_control_id); await call.speak({ payload: welcomePrompt, voice: "male", language: language, }); } catch (err) { console.log("Error answering a call:", err.message); } }; ``` Later, you can observe DTMF digits received to choose the next action in your call flow: ```javascript const call_dtmf_received = async (req) => { const { payload } = req.body.data; const call = new telnyx.Call({ call_control_id: payload.call_control_id, }); console.log("DTMF received:", payload.digit); if (payload.digit === "1") { console.log("Transferring call to external number:", transferNumber); await call.transfer({ to: transferNumber, }); } else if (payload.digit === "2") { const queueName = "Sales"; console.log("Transferring call to a queue: " + queueName); await call.enqueue({ queue_name: queueName, }); } }; ``` When the call is enqueued, you can play a prompt and music to a caller waiting for an available agent. At that stage, you should update your frontend interface in a queue view with information about the new incoming call. You can use the WebSocket interface to emit data to the agent desktop application. ```javascript const call_enqueued = async (req) => { const { payload } = req.body.data; const call = new telnyx.Call({ call_control_id: payload.call_control_id, }); console.log( `Call ${payload.call_control_id} enqueued in ${payload.queue} queue` ); try { await call.speak({ payload: "Please wait while we connect you to an agent", voice: "male", language: "en-US", }); await call.playback_start({ audio_url: `https://${process.env.API_SERVER_URL}/audio/queue_music.mp3`, }); const emitObj = { type: "call-enqueued", payload: payload, }; await Socket.io.emit(JSON.stringify(emitObj)); } catch (error) { console.log("Error has occurred on call enqueued event:", error.message); } }; ``` Based on the other event types, additional actions may be performed according to your designed call flow. Please refer to our [Voice API documentation](/api-reference/call-commands/dial) to check all your actions in your call scenario. ## Next steps - Review [WebRTC authentication](https://developers.telnyx.com/docs/development/webrtc/auth/credential-connections) options. - Explore [Call Control API](/api-reference/call-control-applications/list-call-control-applications) documentation. - Learn more about [webhook fundamentals](https://developers.telnyx.com/docs/development/api-fundamentals/webhooks/receiving-webhooks). --- ### Outbound Dialer > Source: https://developers.telnyx.com/docs/voice/webrtc/use-cases/outbound-dialer.md Build an automated outbound dialer system that enables agents to make high-volume outbound calls efficiently using Telnyx WebRTC and Call Control API. ## Overview In building an outbound dialer solution leveraging Telnyx WebRTC, enable SIP connection credentials with Park Outbound Calls and webhook events to combine front-end WebRTC functionality with backend voice application. ### Key features **Park Outbound Calls** - Combine front-end WebRTC application with backend voice application using Telnyx Voice APIs. - Enable advanced call flow control and routing. - [Learn more about Park Outbound Calls](https://support.telnyx.com/en/articles/4351104-sip-connection-settings#h_7e20c5a7f7). **Webhook events** - Monitor SIP connection events in real-time. - Receive notifications for call events: dialing, answering, bridging, hang-up, voicemail completion. - Primary/failover URL configuration for reliability. ### Required components 1. WebRTC Client. 2. Backend Server Application. 3. SIP Connection with Park Outbound Calls Enabled (select TeXML option when using the TeXML approach). ## Frontend implementation In a typical front-end WebRTC application, there are many components that should be supported. **Agent status management**: The outbound dialer application should be able to display the agent's current status, such as, but not limited to, Available, Unavailable, Busy, and Offline. This type of management should not only act as an indicator for other agents but also limit agents' ability to transfer calls to unavailable agents. This status should also be correlated with the WebRTC client state. This means that when an agent's status is set to available, the WebRTC client should be fully registered and ready to place outbound calls. This should be handled at a global level, typically using state management frameworks such as [Global Context APIs](https://react.dev/reference/react/useContext) or [Redux](https://redux.js.org/). Here is an example softphone application (WebRTC client) with an option to change its current state. ![Outbound dialer agent status management interface](/assets/images/outbound-dialer-agent-status-management.png) **Call control toolbar**: The toolbar is a set of buttons within your WebRTC client dialer for handling calls, with options like Answer, Hangup, Mute/Unmute, Hold/Unhold, and selecting the caller ID. Here is an example of a dialer component. ![Outbound dialer call control toolbar with call handling options](/assets/images/outbound-dialer-call-control-toolbar.png) Here are the functions written in JavaScript React which would be used to build the above options in the frontend app: ## Backend implementation The backend implementation is a crucial component of a successful call. The following sequence diagram covers a typical outbound call flow using [Telnyx Voice API](https://developers.telnyx.com/docs/voice/programmable-voice/sending-commands). Below the sequence diagram, I describe each step. ![Outbound dialer backend architecture diagram](/assets/images/outbound-dialer-backend.png) ### 1. Client Registers with Telnyx The process starts with the WebRTC client (the Front End App) connecting to Telnyx by sending a `Client.connect (Register)` request. This is essentially the WebRTC client registering with Telnyx to initiate communications. ```javascript function connect() { client = new TelnyxWebRTC.TelnyxRTC({ env: env, login: document.getElementById('username').value, password: document.getElementById('password').value, ringtoneFile: './sounds/incoming_call.mp3', // ringbackFile: './sounds/ringback_tone.mp3', }); if (document.getElementById('audio').checked) { client.enableMicrophone(); } else { client.disableMicrophone(); } client.on('telnyx.ready', function () { btnConnect.classList.add('d-none'); btnDisconnect.classList.remove('d-none'); connectStatus.innerHTML = 'Connected'; startCall.disabled = false; }); //Socket close, error and updating call states ... } ``` ### 2. Initiating a Call Once the WebRTC client is connected, it requests to initiate a call by sending a `Client.newCall(destinationNumber,callerNumber)` method to Telnyx. The request requires the destination number and the caller number. This request is routed from the front-end WebRTC client application to the back-end server application, which acts as the intermediary between the client and Telnyx for controlling call logic. ```javascript //Make Call function makeCall() { const params = { callerName: 'Caller Name', callerNumber: 'Caller Number', destinationNumber: document.getElementById('number').value, // required! audio: document.getElementById('audio').checked, video: document.getElementById('video').checked ? { aspectRatio: 16 / 9 } : false, }; currentCall = client.newCall(params); } ``` ### 3. Dialing PSTN (command) The backend server then instructs Telnyx to dial the destination number in the PSTN using the `Dial PSTN with Dial Command`. This command triggers Telnyx to initiate an outbound call to the PSTN. ```bash curl -X POST https://api.telnyx.com/v2/calls \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer YOUR_API_TOKEN' \ -d '{ "connection_id": "YOUR_CONNECTION_ID", "to": "+E.164 PSTNNUMBER", "from": "+E.164 CALLERNUMBER", "webhook_url": "https://yourserver.app/telnyx-webhooks" }' ``` ### 4. Call Initiated (webhook) Telnyx acknowledges the initiation of the call process by triggering a `call.initiated` webhook to the backend server. This webhook indicates that the call process has started but does not necessarily mean the call has been answered. ```json { "data": { "record_type": "event", "event_type": "call.initiated", "id": "uuid-of-the-event", "occurred_at": "2024-03-25T14:00:00Z", "payload": { "call_control_id": "call_control_id_of_the_initiated_call", "connection_id": "connection_id_used_in_the_call", "call_leg_id": "unique_id_for_call_leg", "custom_headers": [ { "header_name": "X-Custom-Header", "header_value": "CustomValue" } ], "call_session_id": "unique_id_for_the_call_session", "client_state": "optional_client_defined_state", "from": "+12345678901", "to": "+10987654321", "direction": "outgoing", "state": "parked" } } } ``` ### 5. PSTN Outbound Call Telnyx makes the outbound call to the destination number in the PSTN network. ### 6. PSTN Answered (webhook) When the PSTN destination answers the call, Telnyx sends a notification back to the backend server through a `call.answered` webhook, indicating that the call had been successfully answered on the PSTN side. ```json { "data": { "record_type": "event", "event_type": "call.answered", "id": "uuid-of-the-event", "occurred_at": "2024-03-25T13:45:00Z", "payload": { "call_control_id": "call_control_id_of_the_call", "connection_id": "connection_id_used_in_the_call", "call_leg_id": "unique_id_for_call_leg", "call_session_id": "unique_id_for_the_call_session", "client_state": "optional_client_defined_state", "custom_headers": [ { "header_name": "X-Header-Example", "header_value": "HeaderValue" } ], "from": "+12345678901", "to": "+10987654321", "state": "answered" } } } ``` ### 7. Bridging Call Legs (command) After the call is answered, the next step is to bridge the call between the WebRTC client and the PSTN to enable two-way communication. The backend server sends a Bridge Call Legs: `call.bridge(call_control_id)` command to Telnyx, instructing it to connect the two call legs. ```bash curl -X POST https://api.telnyx.com/v2/calls/{call_control_id_WebRTC}/actions/bridge \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer YOUR_API_TOKEN' \ -d '{ "call_control_id": "PSTN_CALL_CONTROL_ID" }' ``` ### 8. Call Bridged (webhook) Once the call legs are successfully bridged, Telnyx triggers a `call.bridged` webhook to the backend server, indicating that the WebRTC agent and the PSTN call are now connected, and the call is in progress. ```json { "data": { "record_type": "event", "event_type": "call.bridged", "id": "uuid-of-the-event", "occurred_at": "2024-03-25T12:34:56Z", "payload": { "call_control_id": "call_control_id_of_the_call", "connection_id": "connection_id_used_in_the_call", "call_leg_id": "unique_id_for_call_leg", "call_session_id": "unique_id_for_the_call_session", "client_state": "optional_client_defined_state", "from": "+12345678901", "to": "+10987654321", "state": "bridged" } } } ``` ### 9. Call In Progress With the bridge established, the WebRTC agent (the user on the front-end client) and the PSTN participant can now communicate. This state continues until either party terminates the call. If the call is ended, Telnyx triggers a `call.hangup` webhook. An example `call.hangup` event is provided below. ```json { "data": { "record_type": "event", "event_type": "call.hangup", "id": "uuid-example-1234", "occurred_at": "2024-03-28T12:34:56Z", "payload": { "call_control_id": "call_control_id_example_5678", "connection_id": "connection_id_example_9012", "call_leg_id": "call_leg_id_example_3456", "call_session_id": "call_session_id_example_7890", "client_state": "example_state", "from": "+12345678901", "to": "+10987654321", "start_time": "2024-03-28T12:00:00Z", "state": "hangup", "hangup_cause": "normal_clearing", "hangup_source": "caller", "sip_hangup_cause": "16" } } } ``` The backend implementation is a crucial component of a successful call. The following sequence diagram covers a typical outbound call flow using Telnyx TeXML API. Below the sequence diagram, I describe each step. ![Outbound dialer backend architecture diagram](/assets/images/outbound-dialer-backend.png) ### 1. Client Registers with Telnyx The process starts with the WebRTC client (the Front End App) connecting to Telnyx by sending a `Client.connect (Register)` request. This is essentially the WebRTC client registering with Telnyx to initiate communications. ```javascript function connect() { client = new TelnyxWebRTC.TelnyxRTC({ env: env, login: document.getElementById('username').value, password: document.getElementById('password').value, ringtoneFile: './sounds/incoming_call.mp3', // ringbackFile: './sounds/ringback_tone.mp3', }); if (document.getElementById('audio').checked) { client.enableMicrophone(); } else { client.disableMicrophone(); } client.on('telnyx.ready', function () { btnConnect.classList.add('d-none'); btnDisconnect.classList.remove('d-none'); connectStatus.innerHTML = 'Connected'; startCall.disabled = false; }); //Socket close, error and updating call states ... } ``` ### 2. Initiating a Call Once the WebRTC client is connected, it requests to initiate a call by sending a `Client.newCall(destinationNumber,callerNumber)` method to Telnyx. The request requires the destination number and the caller number. This request is routed from the front-end WebRTC client application to the back-end server application, which acts as the intermediary between the client and Telnyx for controlling call logic. ```javascript //Make Call function makeCall() { const params = { callerName: 'Caller Name', callerNumber: 'Caller Number', destinationNumber: document.getElementById('number').value, // required! audio: document.getElementById('audio').checked, video: document.getElementById('video').checked ? { aspectRatio: 16 / 9 } : false, }; currentCall = client.newCall(params); } ``` ### 3. Dialing PSTN (command) The backend server then instructs Telnyx to dial the first destination number. This command triggers Telnyx to initiate an outbound call to the PSTN. ```bash curl -L 'https://api.telnyx.com/v2/texml/Accounts/:account_sid/Calls' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer ' \ -d '{ "To": "+13121230000", "From": "+13120001234", "Url": "https://www.example.com/texml.xml", "StatusCallback": "https://www.example.com/statuscallback-listener" }' ``` ### 4. TeXML Dial Verb The Url parameter hits a server that then instructs Telnyx using XML to dial the second PSTN transfer B-leg. The verb triggers Telnyx to initiate an outbound call to the second PSTN leg. ```xml +18771234567 ``` TeXML Dial expected callbacks can be found [here](/docs/voice/programmable-voice/texml-verbs/dial#expected-callbacks). ## Next steps - Review [WebRTC authentication](https://developers.telnyx.com/docs/development/webrtc/auth/credential-connections) options. - Explore [Call Control API](/api-reference/call-control-applications/list-call-control-applications) documentation. - Learn more about [webhook fundamentals](https://developers.telnyx.com/docs/development/api-fundamentals/webhooks/receiving-webhooks). --- ## Troubleshooting ### Call Detail Records > Source: https://developers.telnyx.com/docs/voice/webrtc/troubleshooting/detail-records.md ## Searching for Records Every call between a voice SDK client and Telnyx produces a `webrtc` detail record. They can be searched via [this API](/api-reference/detail-records/search-detail-records). For example, the following query returns `webrtc` detail records of calls made * to/from any clients registered with `myagent01` username * within `today` ```json GET /v2/detail_records?filter[record_type]=webrtc&filter[date_range]=today&filter[auth_username]=myagent01 HTTP/1.1 Host: api.telnyx.com Authorization: Bearer XXX ``` The result may look like this ```json { "data": [ { "fs_channel_id": "f6856af8-3fde-48fa-b3ab-027ca90245b6", "finished_at": "2024-12-11T17:43:24Z", "telnyx_call_control_id": "", "call_sec": 50, "connection_name": "js-sdk-p2", "caller_name": "", "rate": "0.002", "auth_username": "myagent01", "dest_number": "+18008648331", "cld": "+18008648331", "currency": "USD", "id": "4b679aae-b7e7-11ef-9fe0-02420aef3920", "payment_method": "rate-deck", "direction": "outbound", "cli": "+15127376291", "cost": "0.002", "billing_group_name": "", "telnyx_leg_id": "4b679aae-b7e7-11ef-9fe0-02420aef3920", "session_id": "63149763-3850-4f9a-b9cf-7babe1be98f8", "billed_sec": 60, "record_type": "webrtc_detail_record", "tags": "", "call_id": "064d6317-4837-41e2-8795-cfc304ced4d1", "billing_group_id": 60, "country_code": 1, "telnyx_session_id": "4b679edc-b7e7-11ef-adfb-02420aef3920", "connection_id": "2519141575053804765", "started_at": "2024-12-11T17:42:30Z", "source_country_code": 1, "caller_number": "+1512-737-6291" } ], "meta": { "total_pages": 1, "total_results": 1, "page_number": 1, "page_size": 20 } } ``` ## Interpreting Records While most of the fields in the records are self explanatory, the following parameters are given additional exposition. ### IDs in the WebRTC Domain * `session_id` identifies a session, i.e. a successful registration, between an SDK client and Telnyx. * `call_id` * identifies a call between an SDK client and Telnyx * can be generated by the SDK client or Telnyx * has a many-to-one relationship to a session, i.e. a session can have many calls. `call_id` is essential to locate the debug log produced by an SDK client. This is further explained [here](https://developers.telnyx.com/docs/voice/webrtc/troubleshooting/debug-logs). ### IDs in the SIP Domain The following IDs can be used to identify the SIP leg of a voice SDK call. * `telnyx_leg_id` * `telnyx_session_id` * `fs_channel_id` ### IDs in the Programmable Voice Domain If programmable voice (call control or TeXML) is used in the call flow, e.g. parking the outbound webRTC call, the following ID may also be returned in the detail record. * `telnyx_call_control_id` --- ### Debug Logs > Source: https://developers.telnyx.com/docs/voice/webrtc/troubleshooting/debug-logs.md This is a beta feature. The data schema and/or presentation may change without notice. Debug data is collected on the SDK client. It provides empirical data on the call leg between SDK client and Telnyx. ## Availability | SDK | Availability | |--|--| | JS | Available | | iOS Native | Available | | Android Native | Available | | Flutter | Available | ## Enabling Debug Initialize the SDK client with [debug](https://developers.telnyx.com/docs/development/webrtc/js-sdk/interfaces/iclientoptions#debug) set to `true` and output set to `socket`. ## Locating the Debug Data When properly enabled, the SDK client will ship debug data frames to Telnyx over the websocket. The data frames are assembled into a single `json` file and stored in a Telnyx Cloud Storage bucket located in `us-central-1` belonging to the user. The bucket is named `voice-sdk-debug-reports-[USER-ID]` where `USER-ID` is the user's account ID. The objects are named following this schema `[call_id]/rtc_stats_reports/[segment_id]` where `call_id` is the ID identifying the [call leg](https://developers.telnyx.com/docs/voice/webrtc/troubleshooting/detail-records#ids-in-the-webrtc-domain) between the SDK client and Telnyx. In most cases, there is only one data segment. When there is a reconnect between the SDK client and Telnyx, there may be more than one data segment. To illustrate the above point more concretely, consider this example: 1. A call is made from a JS SDK client to a phone number. 2. The WebRTC call record is located using the [detail record API](https://developers.telnyx.com/docs/voice/webrtc/troubleshooting/detail-records#ids-in-the-webrtc-domain). 3. Noting the `call_id`, locate the data using Telnyx Mission Control portal or a [properly configured AWS CLI](https://developers.telnyx.com/docs/cloud-storage/quick-start#option-2-using-aws-cli). ``` user@host ~ % aws s3api list-objects-v2 --bucket voice-sdk-debug-reports-22 --profile "*.telnyxcloudstorage.com" --endpoint-url https://us-central-1.telnyxcloudstorage.com --output table --prefix 064d6317-4837-41e2-8795-cfc304ced4d1 ------------------------------------------------------------------------------------------------------------------------ | ListObjectsV2 | +---------------------------------------------------------------------------------+------------------------------------+ | RequestCharged | None | +---------------------------------------------------------------------------------+------------------------------------+ || Contents || |+--------------+-----------------------------------------------------------------------------------------------------+| || ETag | "c351226c014f9589c11b43fa47152374" || || Key | 064d6317-4837-41e2-8795-cfc304ced4d1/rtc_stats_reports/0654064a-0f09-4b33-8f3e-66cd89941abb.json || || LastModified| 2024-12-11T17:43:25.722000+00:00 || || Size | 318313 || || StorageClass| STANDARD || |+--------------+-----------------------------------------------------------------------------------------------------+| ``` where `prefix` is the `call_id`. ## Visualizing the Data The data can be uploaded and visualized via https://webrtc-debug.telnyx.com/. ![](/assets/images/webrtc-debug.png) ## Interpreting the Data The next section provides addition information on how to use the data to diagnose user issues. --- ### Interpreting Debug Data > Source: https://developers.telnyx.com/docs/voice/webrtc/troubleshooting/interpreting-debug-data.md This is a beta feature with limited availability by SDK type. Data schema and/or presentation may change without notification. To make full use of this guide, the reader is encouraged to complete the following steps in order to have a real world example to follow along. 1. Initiates an outbound call from a [properly configured](https://developers.telnyx.com/docs/voice/webrtc/js-sdk/demo-app) `https://webrtc.telnyx.com/` with debug enabled and data sent over socket. 2. [Locate](https://developers.telnyx.com/docs/voice/webrtc/troubleshooting/debug-logs#locating-the-debug-data) the debug data. 3. Upload the data to `https://webrtc-debug.telnyx.com/`. ## Peer Configuration ![](/assets/images/peer-configuration.png) This section provides data on the configuration of the [RTCPeerConnection](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/RTCPeerConnection). If [`prefetchIceCandidates`](https://developers.telnyx.com/docs/development/webrtc/js-sdk/interfaces/icalloptions#prefetchicecandidates) is disabled, the pool size is set to 0. Otherwise, it's set to 255. If [`forceRelayCandidate`](https://developers.telnyx.com/docs/development/webrtc/js-sdk/interfaces/icalloptions#forcerelaycandidate) is enabled, then transport policy will be set to `relay`. Lastly, by default, Telnyx SDKs use the following endpoints to gather ICE candidates. * `stun.l.google.com` * `stun.telnyx.com` * `turn.telnyx.com` ## ICE Candidates & Candidate Pair ![](/assets/images/ice-candidates.png) This section lists out all the [ICE candidates](https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate) gathered prior to a call is established. There will always be one `remote-candidate` of `host` type offered. This represents the Telnyx's end of the peer connection. There will always be multiple `local-candidate` offered unless `relay` candidate was configured to be used. For a call to be successfully established, at least one `local-candidate` of the following type must be present: * `prflx` * `srflx` * `relay` `host` candidate type cannot be used to establish peer connection over the internet. If no viable `local-candidate` are present, it's highly likely that the SDK client is located on a very restrictive network where all UDP traffic is blocked and access to certain endpoints (turn.telnyx.com) are not allowed. Barring that, there will be one pair of ICE candidates used for this call. ![](/assets/images/candidate-pair.png) ## RTT ![](/assets/images/rtt.png) A high RTT value provides clues to voice delay. ## Packets Lost A high packet lost value provides clues to skipped audio. ![](/assets/images/packet-lost.png) ## Jitter A high jitter value provides clues to inconsistent audio quality throughout the call. ![](/assets/images/jitter.png) ## Other Useful Data If the user is experiencing one way audio, it's worth checking inbound and outbound audio level to corroborate the user's claim. --- ## Migration ### Migration from Twilio > Source: https://developers.telnyx.com/docs/voice/webrtc/migration-from-twilio.md Are you thinking of switching from Twilio to Telnyx? This document describes some key differences between the platforms as well as implementation details for standard SDK features to make your experience as smooth as possible. ## How Twilio’s Voice SDK works With Twilio, a lot of setup is required before you can get started making calls. because users are required to have their own backend in place, **with Telnyx this is not needed**. TwiML is similar to Call Control that Telnyx provides - but it isn’t a requirement when using our Voice SDK. In short, Twilio’s Voice SDK flow is as follows: 1. Your browser / mobile device connects to Twilio 2. Twilio connects to your pre-deployed server node application which can generate a token and receive voice webhooks 3. Twilio sends you a webhook to get TwiML instructions 4. Your backend server node responds with a set of TwiML instructions that you have defined for certain use cases (eg. call a number, connect to a conference) 5. Twilio receives your TwiML instructions and executes them on your behalf. (eg. Dial a number contained in your TwiML instructions) 6. Twilio creates a VoIP connection between your callee and your application. Flow looks like this: Caller > Twillio Voice SDK > Twillio Servers > customer backend (Webhook receiver) > Twillio Servers > Callee ## It's much easier with the Telnyx Voice SDK Telnyx’s Voice SDK has been designed to be as simple as possible to make/receive calls in a matter of minutes. Unlike Twilio, there is no backend setup required. Simply, implement the Voice SDK library on the platform of your choice, and log in with your Telnyx Connection and you’re all set. To summarize: 1. Your browser / mobile device connects to Telnyx 2. You send an invitation from the client 3. If accepted, Telnyx creates a VoIP connection between your callee and your application. Flow looks like this: Caller > Telnyx WebRTC SDK > Telnyx servers > Callee Optionally, if you would like to control the call, like you do with TwiML, you can use Call Control. ## Pricing Below is a comparison table for the United States region. (Note regional prices may vary). SERVICE TELNYX ORIGINATION TWILIO ORIGINATION TELNYX TERMINATION TWILIO TERMINATION Local Calls 0.0070/ min 0.0140/ min 0.0055/ min 0.0085/ min Toll-Free Call 0.0020/ min 0.0140/ min 0.0170/ min 0.0220/ min Browser / App Calling 0.0020/ min 0.0040/ min 0.0020/ min 0.0040/ min SIP Interface 0.0020/ min 0.0040/ min 0.0020/ min 0.0040/ min Secure Media included included included included Sources: https://telnyx.com/pricing/call-control https://www.twilio.com/en-us/voice/pricing/us | [Web](#web) | [Android](#android) | [iOS](#ios) | ----- ## Web ### Comparative Table (Web SDK) Telnyx RTC Twilio Voice SDK Portal and server initial configuration No server setup is required. Create SIP connections Buy a phone number and assign it to a SIP connection. Create a TwiML App and find the TwiML App SID and configure a webhook endpoint to make outbound calls Buy a phone number to make outbound and receive inbound calls and configure a webhook to be able to receive incoming calls. Create an API KEY to be able to generate an Access Token Run a server node application to generate token and to receive the voice webhook to exec dial command. SDK Installation npm i @telnyx/webrtc npm i twillio (backend) npm i @twilio/voice-sdk (front-end) To make calls The SDK is connected using a SIP username or JWT Token It calls directly between the browsers with the command `call.newCall({...})` The SDK is connected using a Token. When connecting it will return a Call object that will send a POST in Twillio Sever to /voice endpoint and in the local backend API it will exec the dial command ### Connect #### Telnyx ```javascript // Initialize the client const client = new TelnyxRTC({ /* Use a JWT to authenticate (recommended) */ login_token: login_token, /* or use your Connection credentials */ // login: username, // password: password, }); // Connect and login client.connect(); ``` #### Twilio ```javascript import twilio from 'twilio'; // Download the helper library from https://www.twilio.com/docs/node/install // Find your Account SID and Auth Token at twilio.com/console // and set the environment variables. See http://twil.io/secure const accountSid = process.env.TWILIO_ACCOUNT_SID; const authToken = process.env.TWILIO_AUTH_TOKEN; const client = twilio(accountSid, authToken); ``` ### Make a Call #### Telnyx ```javascript const call = client.newCall({ // Destination is required and can be a phone number or SIP URI destinationNumber: '18004377950', callerNumber: '155531234567', }); ``` #### Twilio ```javascript client.calls .create({ url: 'https://example.com', to: '+15558675310', from: '+15017122661' }) .then(call => console.log(call.sid)); ``` ### Answer an incoming Call **Note**: in Twilio’s case, a backend server needs to be setup. We have included a Python flask example as well as the client implementation #### Telnyx ```javascript client.on('telnyx.notification', (notification) => { const call = notification.call; if (notification.type === 'callUpdate' && call.state === 'ringing') { call.answer(); } }); ``` #### Twilio ```javascript // Backend Server, in this case Python with flask @app.route('/handle_calls', methods=['POST']) def call(): p.pprint(request.form) response = VoiceResponse() dial = Dial(callerId=twilio_number) if 'To' in request.form and request.form['To'] != twilio_number: print('outbound call') dial.number(request.form['To']) else: print('incoming call') caller = request.form['Caller'] dial = Dial(callerId=caller) dial.client(twilio_number) return str(response.append(dial)) ``` ```javascript // Client device.on("incoming", function (conn) { conn.accept(); }); ``` ## Android ### Comparative Table (Android SDK) Telnyx RTC Twilio Voice SDK Portal and server initial configuration No server setup is required. Create SIP connections Buy a phone number and assign it to a SIP connection. Deployment of a **TwiML**. This will generate an **Application SID**. Create a token using the Application SID. Buy a phone number to make PSTN calls SDK Installation JitPack Maven Central Android Minimum API Level Android 23 (6) and higher Android 16 (4.1) and higher Java Compatibility sourceCompatibility 1.8 targetCompatibility 1.8 sourceCompatibility 1.8 targetCompatibility 1.8 Language Kotlin SDK Kotlin Sample app Compose Android Sample App Java SDK Java Sample app To make calls The client is connected while the app is opened, creating an instance of TelnyxClient. With this instance of TelnyxClient you can create and receive multiple calls The SDK is connected using a Token. When connecting it will return a Call object The SDK creates a call object once you have connected. Meaning you authenticate per call. (Or at least include a valid access token per call, tokens live for an hour) Receiving calls Incoming calls when logged in are handled as socket messages which the SDK is listening for. When receiving an invite socket message, a Call object is made which can be answered or declined. Incoming calls when logged out are handled via FCM. All incoming calls are delivered via FCM Push notifications setup (CLI) Create a firebase server key Create push credential in portal with firebase server key Attach push credential to a SIP connection. Create a firebase server key Use Twillio CLI to create Push Credential Twilio CLI returns a Push Credential ID You can now include Credential ID with access token request via a Voice Grant. Push notification setup on the client application Include firebase in your project with google-services.json that includes server key Generate a FCM token at launch via standard getInstance() method. Include FCM token with either credntialLogin or tokenLogin method. The device can now receive notifications when being called Include firebase in your project with google-services.json that includes server key This access token mentioned above is then used with a Voice.register() method which will register your mobile application with the FCM device token as well as the access token. The device can now receive notifications when being called ### Connect **Note**: in Twilio’s case, for their mobile SDKs, they connect and authenticate per call rather than one initial connection #### Telnyx ```java val telnyxClient = TelnyxClient(context) telnyxClient.connect() telnyxClient.credentialLogin(credentialConfig) ``` #### Twilio ```java val contact = (dialog as AlertDialog).findViewById(R.id.contact) params.put("to", contact.text.toString()) val connectOptions: ConnectOptions = Builder(accessToken) .params(params) .build() activeCall = Voice.connect(this@VoiceActivity, connectOptions, callListener) ``` ### Make a Call #### Telnyx ```java telnyxClient.call.newInvite(callerName, callerNumber, destinationNumber, clientState) ``` #### Twilio ```java val contact = (dialog as AlertDialog).findViewById<EditText>(R.id.contact) params.put("to", contact.text.toString()) val connectOptions: ConnectOptions = Builder(accessToken) .params(params) .build() activeCall = Voice.connect(this@VoiceActivity, connectOptions, callListener) ``` ### Answer an incoming Call #### Telnyx ```java mainViewModel.getSocketResponse() ?.observe(this, object : SocketObserver() { SocketMethod.INVITE.methodName -> { val inviteResponse = data.result as InviteResponse telnyxClient.call.acceptCall(inviteResponse.callId, inviteResponse.callerIdNumber) } }) ``` #### Twilio ```java // Receive Intent from notification: private fun handleIncomingCallIntent(intent: Intent?) { if (intent != null && intent.action != null) { val action = intent.action activeCallInvite = intent.getParcelableExtra(Constants.INCOMING_CALL_INVITE) activeCallNotificationId = intent.getIntExtra(Constants.INCOMING_CALL_NOTIFICATION_ID, 0) when (action) { Constants.ACTION_ACCEPT -> answer() else -> {} } } } // answer the call private fun answer() { activeCallInvite.accept(this, callListener) } ``` ## iOS ### Comparative Table (iOS SDK) Telnyx RTC Twilio Voice SDK Portal and server initial configuration No server setup is required. Create SIP connections Buy a phone number and assign it to a SIP connection. Deployment of a **TwiML**. This will generate an **Application SID**. Create a **token** using the Application SID. Buy a phone number to make PSTN calls SDK Installation Cocoapods SPM: Recommended. Cocoapods Carthage Framework To make calls The client is connected while the app is opened. While connected you can make calls. The SDK is connected using a Token. When connecting it will return a Call object Push notifications setup (Portal) Create an APNS certificate Upload the APNS certificate Assign the APNS certificate to a SIP connection. Create an APNS certificate Upload the APNS certificate: This will create a CR_ID. Assign the CR_ID to the APPLICATION_SID Push notification setup on the client APP To register a device for PN: We need to wait until APNS assign a new token and then connect to the client to send the PN parameters over the login message. Unregister a device from the PN is not supported The last registered device for a SIP connection is the one that will receive the push notification. Register a device for PN: Requires a deviceToken and the PushToken. The device is a relation between the TwiML app, the user and the device. You can unregister to stop getting PN The same user can be registered on multiple devices ### Connect **Note**: in Twilio’s case, for their mobile SDKs, they connect and authenticate per call rather than one initial connection #### Telnyx ```c let telnyxClient = TxClient() do { try telnyxClient.connect(txConfig: txConfigToken) } catch let error { print("ViewController:: connect Error \(error)") } ``` #### Twilio ```c let connectOptions = ConnectOptions(accessToken: accessToken) { builder in builder.params = [twimlParamTo: self.outgoingValue.text ?? ""] builder.uuid = uuid } let call = TwilioVoiceSDK.connect(options: connectOptions, delegate: self) ``` ### Make a Call #### Telnyx ```c self.currentCall = try self.telnyxClient?.newCall(callerName: "Caller name", callerNumber: "155531234567", // Destination is required and can be a phone number or SIP URI destinationNumber: "18004377950", callId: UUID.init()) ``` #### Twilio ```c let connectOptions = ConnectOptions(accessToken: accessToken) { builder in builder.params = [twimlParamTo: self.outgoingValue.text ?? ""] builder.uuid = uuid } let call = TwilioVoiceSDK.connect(options: connectOptions, delegate: self) ``` ### Answer an incoming Call #### Telnyx ```c extension ViewController: TxClientDelegate { //.... func onIncomingCall(call: Call) { // We are automatically answering any incoming call as an example, but // maybe you want to store a reference of the call, and answer the call after a button press. self.myCall = call.answer() } } ``` #### Twilio ```c // Listen for telephony notification (after prior setup) extension ViewController: CXProviderDelegate { func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) { NSLog("provider:performAnswerCallAction:") performAnswerVoiceCall(uuid: action.callUUID) { success in if success { NSLog("performAnswerVoiceCall() successful") } else { NSLog("performAnswerVoiceCall() failed") } } action.fulfill() } } // answer the call func performAnswerVoiceCall(uuid: UUID, completionHandler: @escaping (Bool) -> Void) { guard let callInvite = activeCallInvites[uuid.uuidString] else { NSLog("No CallInvite matches the UUID") return } let acceptOptions = AcceptOptions(callInvite: callInvite) { builder in builder.uuid = callInvite.uuid } let call = callInvite.accept(options: acceptOptions, delegate: self) } ``` --- ## SDKs ### WebRTC JS SDK quickstart > Source: https://developers.telnyx.com/docs/development/webrtc/js-sdk/tutorials/make-your-first-call.md # Quickstart Get the Telnyx WebRTC JS SDK running in your app — make your first call in under 5 minutes. ## Before You Begin You'll need: - A [Telnyx account](https://telnyx.com/sign-up) - Node.js 16+ or a modern browser ### Portal Setup Set up everything you need in the Telnyx Portal — no API calls required. **1. Buy a number** Go to **Numbers → Buy Numbers** in the Portal. Purchase a number in your desired country and area code. **2. Create a Credential Connection** Go to **Call Connections → Create → SIP Credential Connection**. This defines how your WebRTC client authenticates with the SIP network. Give it a name and keep the defaults. **3. Create a Telephony Credential** Go to **Call Connections → [Your Connection] → Credentials → Create**. Each user (or device) needs its own credential. Note the **username** and **password** — you'll use these to generate a JWT. **4. Assign your number to the connection** Go to **Numbers → Your Numbers**, select your number, and assign it to the Credential Connection you created. **5. Generate a JWT** Still in the Credentials section, click **Generate Token** for the credential you created. Copy the JWT — this is what you'll pass to the SDK as `login_token`. For production, generate JWTs from your backend using the API. See [Authenticating Your App](/docs/development/webrtc/js-sdk/how-to/authenticating-your-app) for the full flow. ## Install ```bash npm install @telnyx/webrtc ``` ## Create a Client The SDK connects to Telnyx via WebSocket and establishes WebRTC media sessions. Here's the minimal setup: ```javascript import { TelnyxRTC } from '@telnyx/webrtc'; const client = new TelnyxRTC({ login_token: 'YOUR_JWT_TOKEN', // Generate from your backend }); client.on('telnyx.ready', () => { console.log(' Connected to Telnyx'); }); client.on('telnyx.error', (error) => { console.error(' Connection error:', error.code, error.message); }); client.on('telnyx.notification', (notification) => { // Handle call updates (incoming calls, state changes) console.log('Notification:', notification.type); }); client.connect(); ``` **Always wait for `telnyx.ready` before making calls.** The client needs to establish a WebSocket connection and authenticate before it can place calls. ## Authentication The SDK supports three authentication methods: | Method | Property | Use Case | Security | |--------|----------|----------|----------| | **JWT** (recommended) | `login_token` | Production apps | Token expires in 24h | | **Credential** | `login` + `password` | Call Control apps, development | Long-lived, no rotation | | **Anonymous** | `anonymous_login` | AI assistant connections | No identity, limited features | **JWT (Production):** ```javascript const client = new TelnyxRTC({ login_token: 'eyJhbGciOi...', // From your backend }); ``` **Credential (Call Control):** If you're using Telnyx Call Control, you can generate a SIP credential and use it directly: ```javascript const client = new TelnyxRTC({ login: 'gencred...', // SIP username from Portal password: 'your-password', // SIP password }); ``` Each user should get their own credential to avoid registration conflicts. JWT is still preferred for production — credentials don't expire and can't be rotated without updating the client. **Anonymous (AI Assistants):** ```javascript const client = new TelnyxRTC({ anonymous_login: { target_type: 'ai_assistant', target_id: 'YOUR_AI_ASSISTANT_ID', }, }); ``` Anonymous login connects to an AI assistant without requiring a credential. Use this for click-to-call widgets that connect users directly to an AI agent. For the full authentication guide including JWT generation, token refresh, and security best practices, see [Authenticating Your App](/docs/development/webrtc/js-sdk/how-to/authenticating-your-app). ## Make an Outbound Call ```javascript client.on('telnyx.ready', () => { const call = client.newCall({ destinationNumber: '+12345678900', // E.164 format audio: true, }); // Listen for call state changes call.on('telnyx.notification', (notification) => { switch (notification.call.state) { case 'ringing': console.log(' Ringing...'); break; case 'active': console.log(' Call connected!'); break; case 'hangup': console.log(' Call ended'); break; } }); }); ``` ## Receive an Inbound Call ```javascript client.on('telnyx.notification', (notification) => { if (notification.type === 'callUpdate') { const call = notification.call; if (call.state === 'ringing') { // Incoming call — answer it console.log(' Incoming call from', call.remotePartyNumber); call.answer(); } } }); ``` For more control, show an "Accept/Reject" UI instead of auto-answering. ## Play Audio The SDK handles audio elements automatically, but you can provide your own: ```javascript const call = client.newCall({ destinationNumber: '+12345678900', audio: true, // Optional: provide audio elements for playback remoteElement: document.getElementById('remoteAudio'), localElement: document.getElementById('localAudio'), }); ``` Or let the SDK create them: ```html ``` ## Handle Errors ```javascript import { TELNYX_ERROR_CODES } from '@telnyx/webrtc'; client.on('telnyx.error', (error) => { switch (error.code) { case TELNYX_ERROR_CODES.WEBSOCKET_CONNECTION_FAILED: console.error('WebSocket failed — check network'); break; case TELNYX_ERROR_CODES.ICE_CONNECTION_FAILED: console.error('ICE failed — check firewall/TURN config'); break; default: console.error('Error:', error.code, error.message); } }); ``` See the full [Error Handling Guide](/docs/development/webrtc/js-sdk/reference/sw-events) for all error codes and recommended responses. ## Disconnect Always disconnect when the user leaves or the app unloads: ```javascript // User clicks "logout" document.getElementById('logout').addEventListener('click', () => { client.disconnect(); }); // Page unload (tab close, navigation) window.addEventListener('beforeunload', () => { client.disconnect(); }); ``` ## Next Steps - **[Authentication](/docs/development/webrtc/js-sdk/how-to/authenticating-your-app)** — JWT generation, token refresh, security best practices - **[Call State Machine](/docs/development/webrtc/js-sdk/explanation/call-state-lifecycle)** — Understanding call lifecycle and state transitions - **[Call Options](/docs/development/webrtc/js-sdk/reference/icalloptions)** — Custom headers, ICE config, media control - **[Error Handling](/docs/development/webrtc/js-sdk/reference/sw-events)** — Structured error codes and recovery - **[Best Practices](/docs/development/webrtc/js-sdk/how-to/production-best-practices)** — Production checklist, performance, security - **[Demo App](/docs/development/webrtc/js-sdk/tutorials/make-your-first-call)** — Full working reference application --- ## Quick Reference ```javascript import { TelnyxRTC } from '@telnyx/webrtc'; // 1. Create client const client = new TelnyxRTC({ login_token: 'YOUR_JWT' }); // 2. Listen for events client.on('telnyx.ready', () => { /* Connected */ }); client.on('telnyx.error', (err) => { /* Handle errors */ }); client.on('telnyx.notification', (notif) => { if (notif.type === 'callUpdate' && notif.call.state === 'ringing') { notif.call.answer(); } }); // 3. Connect client.connect(); // 4. Make a call const call = client.newCall({ destinationNumber: '+12345678900' }); // 5. Call control call.hangup(); // End call (async in 2.26+) call.muteAudio(); // Mute microphone call.unmuteAudio(); // Unmute // 6. Disconnect client.disconnect(); ``` --- ### Build a Call Center Agent > Source: https://developers.telnyx.com/docs/development/webrtc/js-sdk/tutorials/build-call-center-agent.md # Build a Call Center Agent This tutorial walks you through building a fully functional call center agent interface. You'll learn how to answer incoming calls, mute/unmute, and place calls on hold — the basics a real agent needs. **Prerequisites:** - Completed [Make Your First Call](/docs/development/webrtc/js-sdk/tutorials/make-your-first-call) - A Telnyx account with a Credential Connection and JWT set up - A phone number routed to your Credential Connection **What you'll build:** A browser-based agent dashboard that: - Receives incoming calls - Shows caller ID - Supports mute and hold - Tracks call duration - Handles multiple calls with hold/resume **This SDK is client-side only.** The WebRTC JS SDK handles real-time audio in the browser — it connects agents to calls, manages call state, and streams media. To route calls, create dial plans, or implement IVR logic, you need a backend application using: - **[Programmable Voice (Call Control)](/docs/v2/call-control)** — Build server-side call flows with the Telnyx API. Create calls, transfer, bridge, and play audio programmatically. - **[TeXML](/docs/voice/texml)** — Telnyx's markup language for voice applications. Define call flows in XML with verbs for dial, gather, play, say, and more. This tutorial assumes you already have a backend routing calls to your agents via one of these methods. --- ## Step 1: Set Up the HTML Create `agent.html`: ```html Call Center Agent

Call Center Agent

Disconnected

Unknown Caller

``` --- ## Step 2: Connect and Authenticate Add a ` ``` --- ## Step 3: Handle Incoming Calls ```javascript function handleNotification(notification) { switch (notification.type) { case 'callUpdate': handleCallUpdate(notification.call); break; case 'userMediaError': alert('Microphone access denied. Please allow microphone access and try again.'); break; } } function handleCallUpdate(call) { switch (call.state) { case 'ringing': if (call.direction === 'inbound') { incomingCall = call; document.getElementById('incoming-from').textContent = `Incoming call from: ${call.remotePartyNumber || 'Unknown'}`; document.getElementById('incoming').style.display = 'block'; } break; case 'active': // Call is connected — add to active calls activeCalls.set(call.id, { call, startTime: Date.now() }); startCallTimer(call.id); renderActiveCalls(); break; case 'held': renderActiveCalls(); break; case 'destroyed': stopCallTimer(call.id); activeCalls.delete(call.id); renderActiveCalls(); break; } } ``` --- ## Step 4: Answer and Reject ```javascript function answerIncoming() { if (incomingCall) { incomingCall.answer(); document.getElementById('incoming').style.display = 'none'; incomingCall = null; } } function rejectIncoming() { if (incomingCall) { incomingCall.hangup(); document.getElementById('incoming').style.display = 'none'; incomingCall = null; } } ``` --- ## Step 5: Call Controls ```javascript function muteCall(callId) { const entry = activeCalls.get(callId); if (entry) { entry.call.mute(); renderActiveCalls(); } } function unmuteCall(callId) { const entry = activeCalls.get(callId); if (entry) { entry.call.unmute(); renderActiveCalls(); } } function holdCall(callId) { const entry = activeCalls.get(callId); if (entry) { entry.call.hold(); } } function unholdCall(callId) { const entry = activeCalls.get(callId); if (entry) { entry.call.unhold(); } } function hangupCall(callId) { const entry = activeCalls.get(callId); if (entry) { entry.call.hangup(); } } ``` --- ## Step 6: Render the Active Calls UI ```javascript function renderActiveCalls() { const container = document.getElementById('active-calls'); container.innerHTML = ''; if (activeCalls.size === 0) { container.innerHTML = '

No active calls

'; return; } activeCalls.forEach((entry, callId) => { const call = entry.call; const isMuted = call.isMuted; // Check mute state const isHeld = call.state === 'held'; const card = document.createElement('div'); card.className = `call-card ${call.state}`; card.innerHTML = `
${call.remotePartyNumber || 'Unknown'} ${call.state} ${isMuted ? ' Muted' : ''}
00:00
${isMuted ? '' : '' } ${isHeld ? '' : '' }
`; container.appendChild(card); }); } ``` --- ## Step 7: Call Timer ```javascript function startCallTimer(callId) { const entry = activeCalls.get(callId); if (!entry) return; const startTime = entry.startTime; const timerElement = () => document.getElementById(`timer-${callId}`); callTimers.set(callId, setInterval(() => { const elapsed = Math.floor((Date.now() - startTime) / 1000); const minutes = String(Math.floor(elapsed / 60)).padStart(2, '0'); const seconds = String(elapsed % 60).padStart(2, '0'); const el = timerElement(); if (el) el.textContent = `${minutes}:${seconds}`; }, 1000)); } function stopCallTimer(callId) { const timer = callTimers.get(callId); if (timer) { clearInterval(timer); callTimers.delete(callId); } } ``` --- ## Step 8: Cleanup ```javascript // Clean up when page closes window.addEventListener('beforeunload', () => { if (client) { client.calls.forEach(call => call.hangup()); client.disconnect(); } }); ``` --- ## What's Next? You now have a working call center agent interface. Here are ways to extend it: **Client-side (this SDK):** | Feature | Guide | |---------|-------| | Auto-answer incoming calls | [ICallOptions](/docs/development/webrtc/js-sdk/reference/icalloptions) — set `autoAnswer: true` | | DTMF (press 1 for sales...) | `call.dtmf('1')` — See [Call Class](/docs/development/webrtc/js-sdk/reference/call) | | Custom SIP headers | [ICallOptions](/docs/development/webrtc/js-sdk/reference/icalloptions) — `customHeaders` | | Call quality monitoring | [Monitor Call Quality](/docs/development/webrtc/js-sdk/how-to/monitor-call-quality) | | Reconnection handling | [Handle Reconnection](/docs/development/webrtc/js-sdk/how-to/handle-reconnection) | | React integration | [Integrate with Frameworks](/docs/development/webrtc/js-sdk/how-to/integrate-with-frameworks) | | Debug call issues | [Debug Call Issues](/docs/development/webrtc/js-sdk/how-to/debug-call-issues) | **Server-side (backend):** | Feature | Guide | |---------|-------| | Route calls to agents | [Programmable Voice](/docs/v2/call-control) — Call Control API | | Build IVR menus | [TeXML](/docs/voice/texml) — ``, ``, `` | | Transfer and bridge calls | [Call Control Transfer](/docs/v2/calls/call-actions#transfer) | | Queue and distribute calls | [Call Control Queues](/docs/v2/call-control/queues) | --- ## See Also - [Make Your First Call](/docs/development/webrtc/js-sdk/tutorials/make-your-first-call) — Basic tutorial - [Programmable Voice](/docs/v2/call-control) — Server-side call management - [TeXML](/docs/voice/texml) — XML-based voice applications - [Production Best Practices](/docs/development/webrtc/js-sdk/how-to/production-best-practices) — Deployment guide --- ### WebRTC JS SDK authentication > Source: https://developers.telnyx.com/docs/development/webrtc/js-sdk/how-to/authenticating-your-app.md # Authentication The Telnyx WebRTC SDK supports three authentication methods. **Use JWT for all production applications.** ## Overview | Method | IClientOptions | Use Case | Security | Identity | |--------|---------------|----------|----------|----------| | **JWT** | `login_token` | Production | Token expires in 24h | Per-user | | **Credential** | `login` + `password` | Development only | Long-lived, no rotation | Per-credential | | **Anonymous** | `anonymous_login` (object) | AI assistant connections | No SIP identity | Per-assistant | **Use JWT (`login_token`) for all production applications.** Credentials (`login` + `password`) are long-lived with no automatic rotation. JWTs expire after 24 hours and can be refreshed via `TOKEN_EXPIRING_SOON`. --- ## Method 1: JWT (Recommended) JWT is the most secure authentication method. You generate a short-lived token on your backend and pass it to the SDK. ### How It Works ```mermaid sequenceDiagram participant Browser participant YourBackend participant TelnyxAPI participant rtc.telnyx.com Browser->>YourBackend: Request JWT YourBackend->>TelnyxAPI: POST /telephony_credentials/{id}/token TelnyxAPI-->>YourBackend: JWT (expires in 24h) YourBackend-->>Browser: JWT Browser->>rtc.telnyx.com: new TelnyxRTC({ login_token: jwt }) rtc.telnyx.com-->>Browser: telnyx.ready ``` ### Step 1: Create a Credential Connection Create a SIP Credential Connection in the Telnyx Portal or via API: ```bash curl -X POST https://api.telnyx.com/v2/credential_connections \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "connection_name": "My WebRTC Connection", "transport_protocol": "TLS", "sip_uri_calling_preference": "enabled", "sip_uri_calling_region": "any" }' ``` See [Credential Connections](/docs/development/webrtc/js-sdk/how-to/authenticating-your-app) for full configuration options. ### Step 2: Create a Telephony Credential Each user needs their **own credential**. Never share one credential across multiple users. ```bash curl -X POST https://api.telnyx.com/v2/telephony_credentials \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "connection_id": "YOUR_CONNECTION_ID", "name": "user-123" }' ``` See [Telephony Credentials](/docs/development/webrtc/js-sdk/how-to/authenticating-your-app) for full CRUD operations. ### Step 3: Generate a JWT Generate the JWT on your **backend** — never on the client. This requires your API key. ```bash curl -X POST https://api.telnyx.com/v2/telephony_credentials/{credential_id}/token \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response:** ``` eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJ0ZWxueXhfdGVsZXBob255IiwiZXhwIjox... ``` This token expires in **24 hours**. You must handle refresh (see below). **Node.js example:** ```javascript import Telnyx from 'telnyx'; const telnyx = new Telnyx(process.env.TELNYX_API_KEY); // Express endpoint: return JWT to authenticated user app.get('/api/telnyx-token', async (req, res) => { // Use the credential ID associated with this user const credentialId = getUserCredentialId(req.user.id); try { const token = await telnyx.telephonyCredentials.createToken(credentialId); res.send(token); } catch (err) { res.status(500).send({ error: 'Failed to generate token' }); } }); ``` ### Step 4: Use JWT in the SDK ```javascript import { TelnyxRTC } from '@telnyx/webrtc'; // Fetch JWT from your backend const jwt = await fetch('/api/telnyx-token').then(r => r.text()); const client = new TelnyxRTC({ login_token: jwt, }); client.connect(); ``` ### Token Refresh JWTs expire after 24 hours. Handle the `TOKEN_EXPIRING_SOON` warning to refresh without dropping the connection: ```javascript import { TELNYX_WARNING_CODES } from '@telnyx/webrtc'; client.on('telnyx.warning', async (warning) => { if (warning.code === 34001) { console.log('Token expiring soon — refreshing...'); try { const newToken = await fetch('/api/telnyx-token').then(r => r.text()); // Refresh token without reconnecting client.updateToken(newToken); console.log('Token refreshed '); } catch (err) { console.error('Failed to refresh token:', err); // Force reconnect if refresh fails client.disconnect(); client.connect(); } } }); ``` Start refreshing tokens at least **1 hour before expiry**. The `TOKEN_EXPIRING_SOON` warning fires ~1 hour before expiration. --- ## Method 2: Credential (Development Only) Use `login` + `password` for local development and testing only. ```javascript const client = new TelnyxRTC({ login: 'gencred...', // SIP username from Telephony Credential password: 'your-password', // SIP password }); ``` **When to use:** - Local development and testing - Quick prototyping before setting up JWT infrastructure **When NOT to use:** - Production applications - Multi-user scenarios where each user needs their own identity - Any environment where you need automatic token rotation The `login` value is the `sip_username` from a Telephony Credential (e.g., `gencrednb4ADiBVjsvgvxem0OwkeNfryiIwhaUSJMJXjiwY3Y`). The `password` is set when creating the credential. --- ## Method 3: Anonymous (AI Assistants) Connect to an AI assistant without requiring a credential. The `anonymous_login` option accepts an object specifying the target: ```javascript const client = new TelnyxRTC({ anonymous_login: { target_type: 'ai_assistant', target_id: 'YOUR_AI_ASSISTANT_ID', }, }); ``` **Use cases:** - Click-to-call widgets connecting users directly to an AI assistant - Embedding voice AI in web apps without managing credentials **Limitations:** - Cannot receive inbound calls - No SIP identity — calls are outbound to the specified AI assistant only - Limited call control features ### Continue a conversation Pass a `conversation_id` to resume an existing conversation with the AI assistant: ```javascript const client = new TelnyxRTC({ anonymous_login: { target_type: 'ai_assistant', target_id: 'YOUR_AI_ASSISTANT_ID', target_params: { conversation_id: 'conv_xyz789', }, }, }); ``` --- ## Credential Hierarchy Understanding how Telnyx auth resources relate to each other: ```mermaid graph TD A[Credential Connection] --> B1[Telephony Credential 1] A --> B2[Telephony Credential 2] A --> B3[Telephony Credential N] B1 --> C1[JWT 1 → User A] B2 --> C2[JWT 2 → User B] B3 --> C3[JWT 3 → User C] ``` - **Credential Connection** — SIP-level configuration (transport, codecs, webhook) - **Telephony Credential** — Individual identity (one per user) - **JWT** — Short-lived token generated from a credential **One credential per user.** Never share a credential across multiple users. Each user should have their own credential and their own JWT. Sharing credentials causes registration conflicts — only the most recently connected device receives inbound calls. --- ## Common Mistakes | Don't | Do | |----------|-------| | Use `login` + `password` in production | Use `login_token` (JWT) | | Share one credential across users | Create one credential per user | | Generate JWT on the client side | Generate JWT on your backend | | Ignore token expiry | Handle `TOKEN_EXPIRING_SOON` | | Hardcode JWTs in source code | Fetch JWTs from your backend at runtime | | Store API keys in client code | Keep API keys server-side only | --- ## Server-Side Token Generation Here's a complete Node.js/Express endpoint for generating JWTs: ```javascript import express from 'express'; import Telnyx from 'telnyx'; const app = express(); const telnyx = new Telnyx(process.env.TELNYX_API_KEY); // Map your user IDs to Telnyx credential IDs // In production, store this in your database const userCredentialMap = { 'user-123': 'credential-abc', 'user-456': 'credential-def', }; app.get('/api/telnyx-token', async (req, res) => { // Authenticate user (your auth middleware) const userId = req.user?.id; if (!userId) { return res.status(401).send({ error: 'Not authenticated' }); } const credentialId = userCredentialMap[userId]; if (!credentialId) { return res.status(404).send({ error: 'No credential found for user' }); } try { const token = await telnyx.telephonyCredentials.createToken(credentialId); res.set('Cache-Control', 'no-store'); // Never cache JWTs res.send(token); } catch (err) { console.error('JWT generation failed:', err); res.status(500).send({ error: 'Token generation failed' }); } }); app.listen(3000); ``` --- ## See Also - [IClientOptions](/docs/development/webrtc/js-sdk/interfaces/iclientoptions) — Full client configuration - [Quickstart](/docs/development/webrtc/js-sdk/quickstart) — Get started in 5 minutes - [Credential Connections API](/api-reference/credential-connections) — Create connections via API - [Telephony Credentials API](/api-reference/credentials) — Manage credentials via API - [Create Access Token API](/api-reference/access-tokens/create-an-access-token) — Generate JWTs via API - [Best Practices](/docs/development/webrtc/js-sdk/how-to/production-best-practices#security) — Security best practices --- ### Network Connectivity Requirements > Source: https://developers.telnyx.com/docs/development/webrtc/js-sdk/how-to/configure-network-firewall.md # Network Connectivity Requirements For the Telnyx WebRTC JS SDK to function properly, the client must be able to reach Telnyx's signaling and media infrastructure. --- ## Overview The SDK requires connectivity to three types of endpoints: ```mermaid graph LR A[Client Browser] -->|WebSocket WSS| B[rtc.telnyx.com:443] A -->|STUN UDP| C[stun.telnyx.com:3478] A -->|TURN UDP/TCP| D[turn.telnyx.com:3478] A -->|TURNS TLS/443| F[turn2.telnyx.com:443] B --> E[Telnyx SIP Platform] D --> E F --> E ``` --- ## Signaling The SDK uses a persistent WebSocket connection for call signaling (invite, answer, hangup, etc.). | Property | Value | |----------|-------| | **Host** | `rtc.telnyx.com` | | **Port** | 443 | | **Protocol** | WSS (WebSocket Secure / TLS) | | **Direction** | Outbound | **Requirements:** - Outbound WebSocket connections must be allowed on port 443 - No HTTP long-polling fallback — WebSocket is required - Connection must remain open for the duration of the session **Custom endpoint:** You can override the signaling server using [IClientOptions](/docs/development/webrtc/js-sdk/interfaces/iclientoptions) `env` property, but this is not recommended for production. --- ## STUN STUN servers help the client discover its public IP address for ICE negotiation. | Property | Value | |----------|-------| | **Primary** | `stun.telnyx.com:3478` | | **Fallback** | `stun.l.google.com:19302` | | **Protocol** | UDP | | **Direction** | Outbound | The SDK automatically uses these STUN servers. No configuration required. --- ## TURN TURN servers relay media when direct peer-to-peer connectivity is not possible (e.g., symmetric NAT, restrictive firewalls). | Property | Value | |----------|-------| | **Host (UDP/TCP)** | `turn.telnyx.com` | | **Host (TURNS)** | `turn2.telnyx.com` | | **Port (UDP)** | 3478 | | **Port (TCP)** | 3478 | | **Port (TURNS)** | 443 | | **Protocol** | UDP (preferred) / TCP (fallback) / TLS over 443 (last resort) | | **Authentication** | Automatic (long-term credentials) | | **Direction** | Outbound | The SDK automatically provisions TURN credentials. No manual configuration required. **UDP vs TCP vs TURNS/443:** - **UDP** (preferred) — Lower latency, better for real-time audio - **TCP** (fallback) — Higher latency, used when UDP is blocked - **TURNS over 443** (last resort) — TURN over TLS on port 443, used when both UDP and TCP/3478 are blocked by restrictive firewalls or proxies TURN credentials are automatically provisioned by the SDK. You do not need to configure TURN usernames or passwords. Starting with SDK v2.27.4, the default ICE server list includes a TURNS (TURN over TLS) entry on port 443 (`turns:turn2.telnyx.com:443`) in addition to the existing TURN UDP/3478 and TCP/3478 entries. This provides a last-resort relay fallback for networks that block both UDP/3478 and TCP/3478 but allow outbound TCP/443. --- ## Firewall Configuration ### Minimum required rules | Direction | Destination | Port | Protocol | Purpose | |-----------|-------------|------|----------|---------| | Outbound | `rtc.telnyx.com` | 443 | WSS | Signaling | | Outbound | `stun.telnyx.com` | 3478 | UDP | STUN | | Outbound | `turn.telnyx.com` | 3478 | UDP | TURN (media relay, preferred) | | Outbound | `turn.telnyx.com` | 3478 | TCP | TURN (fallback) | | Outbound | `turn2.telnyx.com` | 443 | TLS | TURN over TLS (restrictive-network fallback) | ### Optional but recommended | Direction | Destination | Port | Protocol | Purpose | |-----------|-------------|------|----------|---------| | Outbound | `stun.l.google.com` | 19302 | UDP | STUN fallback | ### Media ports RTP media uses dynamic ports allocated by the browser. These are ephemeral and cannot be whitelisted by port number. Instead: - **Ensure TURN is accessible** — TURN handles media relay when direct connectivity fails - **Allow UDP outbound** to Telnyx media servers (the `remote_media_ip` seen in SDP) - **Don't restrict outbound UDP to specific ports** — this will break WebRTC --- ## Restrictive Network Scenarios ### STUN fails (error 701) **Symptom:** Client cannot discover its public IP. No `srflx` or `prflx` ICE candidates. **Fix:** 1. Check firewall allows UDP to `stun.telnyx.com:3478` 2. If STUN is blocked, TURN may still work — the SDK falls back automatically 3. If both STUN and TURN are blocked, calls cannot connect ### TURN fails **Symptom:** Client is on a restrictive network (symmetric NAT), can't get `relay` candidates. **Fix:** 1. Check firewall allows UDP to `turn.telnyx.com:3478` 2. If UDP is blocked, check firewall allows TCP to `turn.telnyx.com:3478` 3. If both UDP/3478 and TCP/3478 are blocked, TURNS over TLS on port 443 to `turn2.telnyx.com` will work — this is included in the default ICE server list since SDK v2.27.4 4. If all TURN paths are blocked, use `forceRelayCandidate: true` to skip direct connectivity attempts: ```javascript const call = client.newCall({ destinationNumber: '+12345678900', forceRelayCandidate: true, }); ``` ### Custom ICE servers with `TELNYX_ICE_SERVERS` Starting with SDK v2.27.4, the SDK exports a public `TELNYX_ICE_SERVERS` catalog of ready-to-use ICE server entries. Import it and compose any combination into the `iceServers` option to override the defaults: ```javascript import { TelnyxRTC, TELNYX_ICE_SERVERS } from '@telnyx/webrtc'; const client = new TelnyxRTC({ login_token: jwt, iceServers: [ TELNYX_ICE_SERVERS.TELNYX_STUN, TELNYX_ICE_SERVERS.TELNYX_TURN_UDP_3478, TELNYX_ICE_SERVERS.TELNYX_TURNS_TCP_443, ], }); ``` Available entries: | Constant | URL | Protocol | |----------|-----|----------| | `TELNYX_ICE_SERVERS.GOOGLE_STUN` | `stun:stun.l.google.com:19302` | UDP | | `TELNYX_ICE_SERVERS.TELNYX_STUN` | `stun:stun.telnyx.com:3478` | UDP | | `TELNYX_ICE_SERVERS.TELNYX_TURN_UDP_3478` | `turn:turn.telnyx.com:3478?transport=udp` | UDP | | `TELNYX_ICE_SERVERS.TELNYX_TURN_TCP_3478` | `turn:turn.telnyx.com:3478?transport=tcp` | TCP | | `TELNYX_ICE_SERVERS.TELNYX_TURNS_TCP_443` | `turns:turn2.telnyx.com:443` | TLS | When you omit `iceServers`, the SDK uses its built-in defaults (`DEFAULT_PROD_ICE_SERVERS`) which include STUN + TURN UDP/3478 + TURN TCP/3478 + TURNS/443. Providing an explicit `iceServers` array replaces the defaults entirely. ### Corporate VPN **Symptom:** Calls fail or have poor quality through VPN. **Fix:** 1. Whitelist `rtc.telnyx.com`, `stun.telnyx.com`, `turn.telnyx.com`, and `turn2.telnyx.com` in VPN split-tunneling config 2. Ensure VPN doesn't block UDP traffic to TURN servers or TLS to `turn2.telnyx.com:443` 3. Consider split-tunneling so WebRTC traffic bypasses the VPN ### Docker / Container environments **Symptom:** STUN errors, no ICE candidates, one-way audio. **Fix:** 1. Docker's default bridge network (`172.x` or `10.x`) can interfere with ICE candidate gathering 2. Use `--network host` mode for the container 3. Or configure the Docker network to use the host's network stack --- ## Testing Connectivity ### Quick test Open your browser's DevTools console and run: ```javascript // Test WebSocket signaling const ws = new WebSocket('wss://rtc.telnyx.com:443'); ws.onopen = () => console.log(' WebSocket OK'); ws.onerror = () => console.error(' WebSocket FAILED'); // Test STUN + TURN (including TURNS/443) const pc = new RTCPeerConnection({ iceServers: [ { urls: 'stun:stun.telnyx.com:3478' }, { urls: 'turn:turn.telnyx.com:3478?transport=udp', username: 'testuser', credential: 'testpassword', }, { urls: 'turns:turn2.telnyx.com:443', username: 'testuser', credential: 'testpassword', }, ], }); pc.createDataChannel('test'); pc.createOffer().then(offer => pc.setLocalDescription(offer)); pc.onicecandidate = (e) => { if (e.candidate) { const type = e.candidate.type; console.log(`ICE candidate type: ${type}`); if (type === 'srflx') console.log(' STUN works'); if (type === 'relay') console.log(' TURN works'); } }; ``` ### Debug tools - **SDK debug mode:** Set `debug: true` and `debugOutput: 'socket'` in [IClientOptions](/docs/development/webrtc/js-sdk/interfaces/iclientoptions) - **Debug visualizer:** Upload debug data to `https://webrtc-debug.telnyx.com/` - **Call reports:** Enable `enableCallReports: true` for programmatic access to ICE stats See [Debug Data & Call Quality Analysis](/docs/development/webrtc/js-sdk/how-to/debug-call-issues) for interpreting debug output. --- ## Bandwidth Requirements | Codec | Bitrate | Notes | |-------|---------|-------| | Opus | 6-510 kbps | Default, adaptive. Typical: ~30 kbps | | PCMU (G.711) | 64 kbps | Fallback codec | | PCMA (G.711) | 64 kbps | Fallback codec | **Recommended minimum bandwidth per call:** - **Audio only:** 100 kbps (including overhead) - **With video:** 500-2000 kbps depending on resolution **RTT requirements:** | Quality | RTT | |---------|-----| | Excellent | < 100ms | | Good | < 150ms | | Acceptable | < 300ms | | Poor | > 300ms | --- ## See Also - [IClientOptions](/docs/development/webrtc/js-sdk/interfaces/iclientoptions) — ICE and network configuration - [Debug Data & Call Quality Analysis](/docs/development/webrtc/js-sdk/how-to/debug-call-issues) — Interpreting ICE and quality data - [Best Practices](/docs/development/webrtc/js-sdk/how-to/production-best-practices) — Production deployment guide - [Error Handling](/docs/development/webrtc/js-sdk/how-to/error-handling) — ICE and WebSocket error codes --- ### Device Management > Source: https://developers.telnyx.com/docs/development/webrtc/js-sdk/how-to/switch-audio-devices.md # Device Management The Telnyx WebRTC JS SDK uses the browser's `MediaDevices` API for audio device management. This guide covers selecting devices, switching mid-call, and handling permission changes. --- ## Enumerate Devices List available audio input and output devices: ```javascript const devices = await navigator.mediaDevices.enumerateDevices(); const microphones = devices.filter(d => d.kind === 'audioinput'); const speakers = devices.filter(d => d.kind === 'audiooutput'); microphones.forEach((mic, i) => { console.log(`Mic ${i}: ${mic.label} (${mic.deviceId})`); }); speakers.forEach((speaker, i) => { console.log(`Speaker ${i}: ${speaker.label} (${speaker.deviceId})`); }); ``` Device labels are only available after the user grants microphone permission. Before permission, `label` is an empty string and `deviceId` is a placeholder. --- ## Request Permissions Before you can select a specific device, the user must grant microphone access: ```javascript try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); // Permission granted — device labels now available stream.getTracks().forEach(track => track.stop()); // Release immediately } catch (err) { if (err.name === 'NotAllowedError') { console.error('User denied microphone permission'); } else if (err.name === 'NotFoundError') { console.error('No microphone found'); } } ``` --- ## Select a Specific Device ### When placing a call ```javascript // Get the device ID first const devices = await navigator.mediaDevices.getUserMedia({ audio: true }); const micDeviceId = devices.getAudioTracks()[0].getSettings().deviceId; devices.getTracks().forEach(t => t.stop()); // Use it when placing the call const call = client.newCall({ destinationNumber: '+12345678900', audio: true, localStream: await navigator.mediaDevices.getUserMedia({ audio: { deviceId: { exact: micDeviceId }, }, }), }); ``` ### Via ICallOptions constraints ```javascript const call = client.newCall({ destinationNumber: '+12345678900', audio: true, // The SDK will request this specific device }); ``` --- ## Switch Devices Mid-Call Replace the audio track on an active PeerConnection: ```javascript async function switchMicrophone(newDeviceId) { if (!call?.peerConnection) return; // Get new stream with the selected device const newStream = await navigator.mediaDevices.getUserMedia({ audio: { deviceId: { exact: newDeviceId }, }, }); const newTrack = newStream.getAudioTracks()[0]; const sender = call.peerConnection .getSenders() .find(s => s.track?.kind === 'audio'); if (sender) { await sender.replaceTrack(newTrack); console.log('Switched to microphone:', newTrack.label); } } ``` `replaceTrack()` doesn't require renegotiation — the switch is seamless. The remote party won't hear a gap. --- ## Speaker Output Set the audio output device (sink) on the audio element: ```javascript const audioElement = document.getElementById('remoteAudio'); // Check if the browser supports sink selection if (typeof audioElement.sinkId !== 'undefined') { const devices = await navigator.mediaDevices.enumerateDevices(); const speakers = devices.filter(d => d.kind === 'audiooutput'); // Switch to a specific speaker await audioElement.setSinkId(speakers[1].deviceId); } ``` `setSinkId()` is not supported in all browsers. Safari does not support it as of 2026. Check `typeof audioElement.sinkId !== 'undefined'` before using. --- ## Device Change Detection Listen for device changes (headphones plugged in, Bluetooth connected, etc.): ```javascript navigator.mediaDevices.addEventListener('devicechange', async () => { console.log('Audio devices changed'); const devices = await navigator.mediaDevices.enumerateDevices(); const mics = devices.filter(d => d.kind === 'audioinput'); // Update device picker UI updateMicrophoneList(mics); }); ``` **Common scenarios:** - Headphones plugged in → switch output to headphones - Bluetooth headset disconnected → fall back to built-in speaker - USB microphone connected → update device list --- ## Mute vs Device Off Don't confuse muting with device management: | Action | What it does | Remote party hears | |--------|-------------|-------------------| | `call.muteAudio()` | Stops sending audio | Silence | | `track.enabled = false` | Same as mute (lower level) | Silence | | Switching to a different mic | Changes input device | New mic audio | | Revoking mic permission | Browser blocks access | Nothing | --- ## Common Issues ### "Device not found" after permission grant **Cause:** The device list was cached before permission was granted. Labels and real device IDs are only available after `getUserMedia()`. **Fix:** Re-enumerate devices after permission is granted: ```javascript const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); stream.getTracks().forEach(t => t.stop()); // Now enumerate — labels and real IDs are available const devices = await navigator.mediaDevices.enumerateDevices(); ``` ### Echo or feedback **Cause:** Speaker output is being picked up by the microphone (especially with built-in speakers + mic on laptops). **Fix:** 1. Use echo cancellation (enabled by default in most browsers) 2. Recommend headphones for long calls 3. Use `call.muteAudio()` when not speaking ### Device disappears mid-call **Cause:** Bluetooth disconnected, USB device unplugged. **Fix:** 1. Listen for `devicechange` events 2. Fall back to the default device: ```javascript navigator.mediaDevices.addEventListener('devicechange', async () => { const devices = await navigator.mediaDevices.enumerateDevices(); const defaultMic = devices.find(d => d.kind === 'audioinput'); if (defaultMic) { await switchMicrophone(defaultMic.deviceId); } }); ``` --- ## See Also - [Call Class](/docs/development/webrtc/js-sdk/classes/call) — `muteAudio()`, `unmuteAudio()` - [ICallOptions](/docs/development/webrtc/js-sdk/interfaces/icalloptions) — `localStream` for custom device selection - [Best Practices](/docs/development/webrtc/js-sdk/how-to/production-best-practices) — Production deployment guide --- ### Manage Multiple Calls > Source: https://developers.telnyx.com/docs/development/webrtc/js-sdk/how-to/manage-multiple-calls.md # Manage Multiple Calls The Telnyx WebRTC JS SDK supports multiple simultaneous calls within a single `TelnyxRTC` client session. This guide covers concurrent call management, per-call media elements, call waiting, hold/transfer patterns, and the warnings the SDK emits when multiple calls are active. The SDK does **not** recommend having multiple active calls simultaneously and cannot guarantee stable behavior in all scenarios. The supported pattern is accepting and holding an inbound call while finishing an active one — the SDK handles this well. Running two fully active calls at the same time (both with bidirectional audio) is not recommended and may produce unpredictable media or signaling behavior. --- ## Overview A single `TelnyxRTC` instance connected to `rtc.telnyx.com` can have multiple active calls at the same time. Each call has its own: - **Call ID** (`call.id`) — unique per call leg - **Direction** (`call.direction`) — `inbound` or `outbound` - **State** (`call.state`) — `ringing`, `trying`, `active`, `held`, `hangup`, `destroyed` - **PeerConnection** — independent `RTCPeerConnection` per call - **Media element** — `remoteElement` / `localElement` (see [Per-call media elements](#per-call-media-elements)) ```javascript const client = new TelnyxRTC({ login_token: jwt }); client.connect(); // client.calls is an array of all call objects client.on('telnyx.notification', (notification) => { if (notification.type === 'callUpdate') { console.log(`Active calls: ${client.calls.length}`); client.calls.forEach(call => { console.log(` ${call.id}: ${call.direction} ${call.state}`); }); } }); ``` --- ## Per-call media elements Starting with SDK v2.27.4, you can assign a distinct `remoteElement` (and `localElement`) per call. This is essential for concurrent calls — without it, all calls share the same audio element and the SDK emits a `SHARED_REMOTE_ELEMENT_OVERWRITE` warning when a second call overwrites the first call's stream. ### Why per-call elements matter | Scenario | Shared element | Per-call element | |----------|---------------|-----------------| | **Two active calls** | Second call overwrites first call's audio; `SHARED_REMOTE_ELEMENT_OVERWRITE` warning | Each call plays through its own `