# Telnyx Calling: WebRTC — Full Documentation > Complete page content for WebRTC (Calling section) of the Telnyx developer docs (https://developers.telnyx.com). > Root index: https://developers.telnyx.com/llms.txt · Lightweight index for this subsection: https://telnyx.com/llms/calling/webrtc.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. ![](/img/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" } ``` ## 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) --- ### 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. ## 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 ``` ## SDK Authentication SDKs are authenticated with * `sip_username` which starts with `gencred` * `sip_password` ## 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) --- ### 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 ## SDK Authentication SDKs are authenticated with the JWT. ## 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) --- ## 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/quickstart)). 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](/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/quickstart)). 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](/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](/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](/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`. ![](/img/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. ![](/img/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. ![](/img/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](/img/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](/img/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](/img/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/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/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](/img/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](/img/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](/img/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](/img/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/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/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/voice/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/. ![](/img/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 ![](/img/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/voice/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/voice/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 ![](/img/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. ![](/img/candidate-pair.png) ## RTT ![](/img/rtt.png) A high RTT value provides clues to voice delay. ## Packets Lost A high packet lost value provides clues to skipped audio. ![](/img/packet-lost.png) ## Jitter A high jitter value provides clues to inconsistent audio quality throughout the call. ![](/img/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. --- ## SDKs ### WebRTC JS SDK quickstart > Source: https://developers.telnyx.com/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](/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](/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](/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](/development/webrtc/js-sdk/how-to/authenticating-your-app)** — JWT generation, token refresh, security best practices - **[Call State Machine](/development/webrtc/js-sdk/explanation/call-state-lifecycle)** — Understanding call lifecycle and state transitions - **[Call Options](/development/webrtc/js-sdk/reference/icalloptions)** — Custom headers, ICE config, media control - **[Error Handling](/development/webrtc/js-sdk/reference/sw-events)** — Structured error codes and recovery - **[Best Practices](/development/webrtc/js-sdk/how-to/production-best-practices)** — Production checklist, performance, security - **[Demo App](/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/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](/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 body { font-family: system-ui; max-width: 800px; margin: 40px auto; padding: 0 20px; } .status { padding: 12px; border-radius: 8px; margin-bottom: 16px; } .status.connected { background: #d4edda; color: #155724; } .status.disconnected { background: #f8d7da; color: #721c24; } .call-card { border: 1px solid #ddd; border-radius: 8px; padding: 16px; margin-bottom: 12px; } .call-card.active { border-color: #28a745; background: #f0fff4; } .call-card.held { border-color: #ffc107; background: #fffdf0; } .call-card.ringing { border-color: #007bff; background: #f0f8ff; animation: pulse 1s infinite; } @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.7; } } .controls { display: flex; gap: 8px; margin-top: 12px; } .btn { padding: 8px 16px; border-radius: 6px; border: none; cursor: pointer; font-size: 14px; } .btn-answer { background: #28a745; color: white; } .btn-reject { background: #dc3545; color: white; } .btn-mute { background: #6c757d; color: white; } .btn-hold { background: #ffc107; color: #333; } .btn-hangup { background: #dc3545; color: white; } .timer { font-size: 24px; font-weight: bold; font-variant-numeric: tabular-nums; } #incoming { display: none; } Call Center Agent Disconnected Unknown Caller Answer Reject ``` --- ## Step 2: Connect and Authenticate Add a `` tag and connect: ```html let client = null; let incomingCall = null; let activeCalls = new Map(); // callId → { call, timer, duration } let callTimers = new Map(); async function connect() { // In production, fetch JWT from your backend const response = await fetch('/api/telnyx-token'); const { token } = await response.json(); client = new TelnyxRTC({ login_token: token, enableCallReports: true, }); // Wait for connection client.on('telnyx.ready', () => { document.getElementById('connection-status').className = 'status connected'; document.getElementById('connection-status').textContent = 'Connected — Waiting for calls'; }); client.on('telnyx.socket.close', () => { document.getElementById('connection-status').className = 'status disconnected'; document.getElementById('connection-status').textContent = 'Disconnected — Reconnecting...'; }); // Handle all notifications client.on('telnyx.notification', handleNotification); client.connect(); } connect(); ``` --- ## 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 ? 'Unmute' : 'Mute' } ${isHeld ? 'Resume' : 'Hold' } Hang Up `; 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](/development/webrtc/js-sdk/reference/icalloptions) — set `autoAnswer: true` | | DTMF (press 1 for sales...) | `call.dtmf('1')` — See [Call Class](/development/webrtc/js-sdk/reference/call) | | Custom SIP headers | [ICallOptions](/development/webrtc/js-sdk/reference/icalloptions) — `customHeaders` | | Call quality monitoring | [Monitor Call Quality](/development/webrtc/js-sdk/how-to/monitor-call-quality) | | Reconnection handling | [Handle Reconnection](/development/webrtc/js-sdk/how-to/handle-reconnection) | | React integration | [Integrate with Frameworks](/development/webrtc/js-sdk/how-to/integrate-with-frameworks) | | Debug call issues | [Debug Call Issues](/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](/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](/development/webrtc/js-sdk/how-to/production-best-practices) — Deployment guide --- ### WebRTC JS SDK authentication > Source: https://developers.telnyx.com/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](/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](/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](/development/webrtc/js-sdk/interfaces/iclientoptions) — Full client configuration - [Quickstart](/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](/development/webrtc/js-sdk/how-to/production-best-practices#security) — Security best practices --- ### Network Connectivity Requirements > Source: https://developers.telnyx.com/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:443] B --> E[Telnyx SIP Platform] D --> 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](/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** | `turn.telnyx.com` | | **Port (UDP)** | 3478 | | **Port (TCP)** | 443 | | **Protocol** | UDP (preferred) / TCP (fallback) | | **Authentication** | Automatic (long-term credentials) | | **Direction** | Outbound | The SDK automatically provisions TURN credentials. No manual configuration required. **UDP vs TCP:** - **UDP** (preferred) — Lower latency, better for real-time audio - **TCP** (fallback) — Higher latency, used only when UDP is blocked TURN credentials are automatically provisioned by the SDK. You do not need to configure TURN usernames or passwords. --- ## 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) | | Outbound | `turn.telnyx.com` | 443 | TCP | TURN (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 TCP/443 to `turn.telnyx.com` 2. If UDP is blocked, TCP TURN will work but with higher latency 3. As a last resort, use `forceRelayCandidate: true` to skip direct connectivity attempts: ```javascript const call = client.newCall({ destinationNumber: '+12345678900', forceRelayCandidate: true, }); ``` ### Corporate VPN **Symptom:** Calls fail or have poor quality through VPN. **Fix:** 1. Whitelist `rtc.telnyx.com`, `stun.telnyx.com`, and `turn.telnyx.com` in VPN split-tunneling config 2. Ensure VPN doesn't block UDP traffic to TURN servers 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 const pc = new RTCPeerConnection({ iceServers: [{ urls: 'stun:stun.telnyx.com:3478' }], }); 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](/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](/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](/development/webrtc/js-sdk/interfaces/iclientoptions) — ICE and network configuration - [Debug Data & Call Quality Analysis](/development/webrtc/js-sdk/how-to/debug-call-issues) — Interpreting ICE and quality data - [Best Practices](/development/webrtc/js-sdk/how-to/production-best-practices) — Production deployment guide - [Error Handling](/development/webrtc/js-sdk/how-to/error-handling) — ICE and WebSocket error codes --- ### Device Management > Source: https://developers.telnyx.com/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](/development/webrtc/js-sdk/classes/call) — `muteAudio()`, `unmuteAudio()` - [ICallOptions](/development/webrtc/js-sdk/interfaces/icalloptions) — `localStream` for custom device selection - [Best Practices](/development/webrtc/js-sdk/how-to/production-best-practices) — Production deployment guide --- ### Call Report Stats > Source: https://developers.telnyx.com/development/webrtc/js-sdk/how-to/monitor-call-quality.md # Call Report Stats The Telnyx WebRTC JS SDK can automatically collect WebRTC statistics during and after calls. Use call reports to monitor quality, diagnose issues, and build real-time quality indicators. --- ## Enabling Call Reports ```javascript const client = new TelnyxRTC({ login_token: jwt, enableCallReports: true, // Required (default: true) callReportInterval: 5000, // Stats every 5 seconds (default) }); ``` | Option | Type | Default | Description | |--------|------|---------|-------------| | `enableCallReports` | `boolean` | `true` | Enable call report collection | | `callReportInterval` | `number` | `5000` | Interval in ms between periodic stats | --- ## Real-Time Stats (`telnyx.stats.frame`) Fires periodically during an active call (every `callReportInterval` ms): ```javascript client.on('telnyx.stats.frame', (stats) => { console.log('RTT:', stats.rtt); console.log('Jitter:', stats.jitter); console.log('Packet loss:', stats.packetLoss); }); ``` ### StatsFrame Properties | Property | Type | Description | |----------|------|-------------| | `rtt` | `number` | Round-trip time in milliseconds | | `jitter` | `number` | Jitter in milliseconds | | `packetLoss` | `number` | Packet loss percentage | | `bytesSent` | `number` | Total bytes sent | | `bytesReceived` | `number` | Total bytes received | | `packetsSent` | `number` | Total RTP packets sent | | `packetsReceived` | `number` | Total RTP packets received | | `packetsLost` | `number` | Total RTP packets lost | | `audioLevel` | `number` | Current audio level (0.0 - 1.0) | | `timestamp` | `number` | Unix timestamp of the measurement | ### Quality Thresholds | Metric | Good | Fair | Poor | |--------|---------|---------|---------| | RTT | < 150ms | 150-300ms | > 300ms | | Jitter | < 20ms | 20-50ms | > 50ms | | Packet Loss | < 1% | 1-3% | > 3% | ### Building a quality indicator ```javascript client.on('telnyx.stats.frame', (stats) => { let quality = 'excellent'; if (stats.rtt > 300 || stats.packetLoss > 3) { quality = 'poor'; } else if (stats.rtt > 150 || stats.packetLoss > 1) { quality = 'fair'; } updateQualityIndicator(quality); }); ``` ### Quality warning events The SDK also emits structured `telnyx.warning` events when quality or connectivity thresholds are crossed. Use these warnings to drive user-facing indicators and collect diagnostics without parsing raw stats yourself: ```javascript import { SwEvent, TELNYX_WARNING_CODES } from '@telnyx/webrtc'; client.on(SwEvent.Warning, ({ warning, callId }) => { switch (warning.code) { case TELNYX_WARNING_CODES.LOW_LOCAL_AUDIO: showMicrophoneWarning(callId); break; case TELNYX_WARNING_CODES.ICE_CANDIDATE_PAIR_CHANGED: logNetworkPathChange(callId); break; } }); ``` `LOW_LOCAL_AUDIO` means RTP may still be flowing, but local microphone level is too low or silent. Ask the user to check microphone selection, mute state, and operating system input gain. `ICE_CANDIDATE_PAIR_CHANGED` means the selected ICE path changed mid-call. The call may continue normally, but frequent changes are useful diagnostics for VPN changes, Wi-Fi handoffs, NAT rebinding, or relay fallback. --- ## End-of-Call Report (`telnyx.stats.report`) Fires when a call ends with a summary of the entire call: ```javascript client.on('telnyx.stats.report', (report) => { console.log('Call ended:', report.callId); console.log('Duration:', report.duration, 'seconds'); console.log('Average RTT:', report.avgRtt); }); ``` --- ## Call Report Stats API For SDK 2.25.20+, call reports are also available via HTTP API: ```bash # Get full call report with ICE data curl "http://voice-sdk-call-report-stats.query.prod.telnyx.io:4000/api/v1/calls/{user_id}/{call_id}" # Get ICE candidate data curl "http://voice-sdk-call-report-stats.query.prod.telnyx.io:4000/api/v1/calls/{user_id}/{call_id}/ice" ``` ### API Response Structure ```json { "data": { "call": { "call_id": "98041520-...", "duration": 45, "sdk_version": "2.26.3", "telnyx_session_id": "...", "telnyx_leg_id": "..." }, "segments": [ { "timestamp": 1712000000, "bytesSent": 12345, "bytesReceived": 23456, "rtt": 45, "jitter": 3, "audioLevel": 0.5 } ], "ice_data": { "transport": { "dtls_state": "connected", "ice_state": "connected", "srtp_cipher": "AES_CM_128_HMAC_SHA1_80" }, "selected_pair": { "local_candidate": { "type": "relay", "ip": "64.16.248.1", "port": 50000 }, "remote_candidate": { "type": "host", "ip": "10.239.207.80", "port": 50001 }, "nominated": true, "state": "succeeded" }, "candidates": [ { "type": "host", "ip": "192.168.1.5", "port": 50000, "timestamp": 1712000000 }, { "type": "srflx", "ip": "203.0.113.5", "port": 50000, "timestamp": 1712000001 }, { "type": "relay", "ip": "64.16.248.1", "port": 50000, "timestamp": 1712000002 } ] }, "logs": ["SDK console log entries if captured"] } } ``` ### Key Fields for Diagnostics | Field | Path | What It Tells You | |-------|------|-------------------| | DTLS state | `ice_data.transport.dtls_state` | `"connected"` = media encrypted , `"connecting"` = DTLS stuck | | ICE state | `ice_data.transport.ice_state` | `"connected"` = ICE worked | | SRTP cipher | `ice_data.transport.srtp_cipher` | Null = no encryption (DTLS failed) | | Selected pair | `ice_data.selected_pair` | Which candidate pair is actually in use | | Candidate types | `ice_data.candidates[].type` | `host` = direct, `srflx` = STUN, `relay` = TURN | --- ## Diagnosing Issues from Call Reports ### DTLS stuck ("connecting") ``` ice_data.transport.dtls_state: "connecting" ice_data.transport.ice_state: "connected" ice_data.transport.srtp_cipher: null ``` **Cause:** ICE succeeded but DTLS handshake failed. Usually a network issue where DTLS packets from one side aren't reaching the other (asymmetric routing, multiple NICs, firewall). **Action:** Check if client has multiple network interfaces. See [Best Practices → Network](/development/webrtc/js-sdk/how-to/production-best-practices#audio-quality). ### All relay candidates ``` ice_data.candidates: [ { type: "relay", ... }, { type: "relay", ... } ] ``` **Cause:** Client can't generate host or srflx candidates. Likely behind strict NAT or VPN. **Action:** Check firewall settings. If expected (e.g., for privacy), set `forceRelayCandidate: true`. ### No candidates at all ``` ice_data.candidates: [] ``` **Cause:** STUN/TURN servers unreachable, or browser denied media permissions before ICE gathering started. **Action:** Check network connectivity to `stun.telnyx.com` and `turn.telnyx.com`. See [Network Requirements](/development/webrtc/js-sdk/how-to/configure-network-firewall). --- ## See Also - [IClientOptions](/development/webrtc/js-sdk/interfaces/iclientoptions) — `enableCallReports`, `callReportInterval` - [Best Practices](/development/webrtc/js-sdk/how-to/production-best-practices#audio-quality) — Quality monitoring guidance - [Network Requirements](/development/webrtc/js-sdk/how-to/configure-network-firewall) — ICE/STUN/TURN configuration - [Debug Data & Call Quality Analysis](/development/webrtc/js-sdk/how-to/debug-call-issues) — Interpreting debug output --- ### Reconnection & Call Recovery > Source: https://developers.telnyx.com/development/webrtc/js-sdk/how-to/handle-reconnection.md # Reconnection & Call Recovery Network interruptions happen — Wi-Fi drops, VPNs reconnect, laptops sleep. The Telnyx WebRTC JS SDK automatically handles reconnection so your users experience minimal disruption. --- ## How Reconnection Works ```mermaid stateDiagram-v2 [*] --> Connected: connect() Connected --> Reconnecting: WebSocket drops Reconnecting --> Connected: Reconnect succeeds Reconnecting --> Disconnected: Reconnect fails (exhausted) Connected --> Disconnected: disconnect() Disconnected --> [*] ``` When the WebSocket connection to `rtc.telnyx.com` drops: 1. **SDK detects the disconnect** via WebSocket `close` or `error` event, or via active-call signaling health checks when the socket is half-dead 2. **SDK attempts reconnection** after a randomized 2-6 second delay 3. **On successful reconnect**, the SDK re-authenticates and re-attaches to existing calls 4. **If reconnection fails** after `maxReconnectAttempts` attempts (default: 10), the `RECONNECTION_EXHAUSTED` error is emitted **No manual intervention required.** The SDK handles this automatically. --- ## When Calls Survive Reconnection Calls can survive a brief WebSocket disconnect if: | Condition | Required | |-----------|----------| | Call was in `active` state | | | `keepConnectionAliveOnSocketClose` is `true` | | | PeerConnection is still alive | | | Reconnect happens within timeout | | **Configuration:** ```javascript const client = new TelnyxRTC({ login_token: jwt, keepConnectionAliveOnSocketClose: true, // Keep PeerConnection alive }); ``` When `keepConnectionAliveOnSocketClose` is enabled: - The PeerConnection (WebRTC media) stays alive even when the WebSocket (signaling) drops - Audio continues flowing during the reconnection attempt - On reconnect, the SDK re-attaches the existing call to the new WebSocket - The call ID may change — use `recoveredCallId` to correlate ### `recoveredCallId` After a successful reconnection, the call may have a new ID. The previous ID is available as `recoveredCallId`: ```javascript client.on('telnyx.notification', (notification) => { if (notification.type === 'callUpdate') { const call = notification.call; if (call.recoveredCallId) { console.log(`Call ${call.recoveredCallId} recovered as ${call.id}`); } } }); ``` --- ## When Calls Don't Survive | Scenario | Why | |----------|-----| | Call was in `ringing` or `requesting` state | Not yet fully established | | `keepConnectionAliveOnSocketClose` is `false` (default) | PeerConnection closed immediately | | Reconnect took too long | PeerConnection timed out | | Network changed completely | DTLS fingerprint no longer matches | When a call doesn't survive reconnection, the SDK emits a `hangup` state for that call. --- ## Handling Reconnection in Your UI ### Show reconnection status ```javascript let isReconnecting = false; client.on('telnyx.socket.close', () => { isReconnecting = true; showReconnectingBanner(); }); client.on('telnyx.ready', () => { if (isReconnecting) { isReconnecting = false; hideReconnectingBanner(); } }); client.on('telnyx.error', (error) => { if (error.code === TELNYX_ERROR_CODES.RECONNECTION_EXHAUSTED) { showDisconnectedMessage(); } }); ``` ### Handle recovered calls ```javascript client.on('telnyx.notification', (notification) => { if (notification.type === 'callUpdate') { const call = notification.call; if (call.recoveredCallId) { // Update your UI state to use the new call ID updateCallInUI(call.recoveredCallId, call.id); } } }); ``` ### Handle signaling and media recovery warnings During an active call, the SDK monitors WebSocket signaling liveness and media flow. If the browser reports the WebSocket as open but signaling stops flowing, the SDK emits `telnyx.warning` and force-closes the socket to trigger reconnect + call reattach. If signaling is healthy but media is unhealthy, the SDK emits a media recovery warning and attempts ICE restart instead. ```javascript import { SwEvent, TELNYX_WARNING_CODES } from '@telnyx/webrtc'; client.on(SwEvent.Warning, ({ warning, callId, reason, source }) => { switch (warning.code) { case TELNYX_WARNING_CODES.SIGNALING_HEALTH_PROBE_TIMEOUT: case TELNYX_WARNING_CODES.SIGNALING_REQUEST_TIMEOUT: case TELNYX_WARNING_CODES.SIGNALING_RECOVERY_REQUIRED: showReconnectingBanner({ reason, source }); break; case TELNYX_WARNING_CODES.MEDIA_RECOVERY_REQUIRED: showMediaReconnectingIndicator(callId); break; } }); ``` For these warnings, keep the current call UI active. Wait for `telnyx.ready`, a recovered `callUpdate`, or a final `hangup` before cleaning up the call. --- ## Inbound Calls After Reconnection After a WebSocket reconnect, the browser may need to re-acquire microphone permissions to receive inbound calls. This is a browser security requirement, not an SDK limitation. ### `mediaPermissionsRecovery` Handle microphone permission failures for inbound calls with a recoverable error pattern. When enabled and `getUserMedia` fails while answering, the SDK emits a recoverable `telnyx.error` event with `resume()` and `reject()` callbacks so your app can prompt the user to fix permissions before the call fails: ```javascript import { TelnyxRTC, isMediaRecoveryErrorEvent } from '@telnyx/webrtc'; const client = new TelnyxRTC({ login_token: jwt, mediaPermissionsRecovery: { enabled: true, timeout: 20000, // Wait up to 20s for user to fix permissions onSuccess: () => console.log('Media recovered'), onError: (err) => console.error('Recovery failed', err), }, }); client.on('telnyx.error', (event) => { if (isMediaRecoveryErrorEvent(event)) { showPermissionDialog({ onContinue: () => event.resume(), onCancel: () => event.reject?.(), }); } }); ``` **How it works:** 1. An inbound call arrives and the user tries to answer 2. `getUserMedia()` fails (permission denied, device busy, etc.) 3. Instead of immediately failing the call, SDK emits a recoverable error with `resume()` and `reject()` callbacks 4. Your app shows a UI prompting the user to grant permissions 5. If the user fixes permissions and you call `resume()`, the SDK retries `getUserMedia()` 6. If the user declines or `timeout` expires, the call is terminated `mediaPermissionsRecovery` only works for inbound calls. Recovery is attempted only when the initial `getUserMedia` call fails while answering. --- ## Explicit Disconnect When a user intentionally disconnects (e.g., signs out), you want to prevent automatic reconnection: ```javascript // Bad — may auto-reconnect client.disconnect(); // Good — prevent auto-reconnect client.clearReconnectToken(); client.disconnect(); ``` `clearReconnectToken()` removes the session token that the SDK uses for automatic reconnection. After calling it, the SDK will not attempt to reconnect. --- ## Common Issues ### Rapid reconnection loops **Symptom:** WebSocket connects and immediately disconnects, repeating rapidly. **Cause:** Usually an authentication issue — the JWT has expired or the credential has been revoked. **Fix:** 1. Check that the JWT is still valid 2. Verify the credential still exists in the Telnyx Portal 3. Check for `telnyx.error` events with `AUTH_FAILED` code ### `RECONNECTION_EXHAUSTED` **Symptom:** SDK stops trying to reconnect after multiple failures. **Cause:** Automatic reconnect reached `maxReconnectAttempts` (default: 10). The SDK uses a randomized 2-6 second delay between attempts; set `maxReconnectAttempts: 0` only if your app should retry indefinitely. **Fix:** 1. Check network connectivity 2. Verify `rtc.telnyx.com` is reachable 3. Offer a manual "Reconnect" button in the UI: ```javascript client.on('telnyx.error', (error) => { if (error.code === TELNYX_ERROR_CODES.RECONNECTION_EXHAUSTED) { showManualReconnectButton(); } }); // Manual reconnect reconnectButton.addEventListener('click', () => { client.connect(); }); ``` ### Calls die after network change (Wi-Fi → Cellular) **Symptom:** Call drops when switching networks, even with `keepConnectionAliveOnSocketClose`. **Cause:** The PeerConnection's ICE candidates are tied to the old network. The new network has different candidates that weren't part of the original negotiation. **Fix:** This is a WebRTC limitation. The SDK attempts ICE restart, but it may not always succeed. The user will need to place a new call. --- ## Configuration Summary | Option | Location | Default | Description | |--------|----------|---------|-------------| | `keepConnectionAliveOnSocketClose` | IClientOptions | `false` | Keep PeerConnection alive during WebSocket reconnect | | `maxReconnectAttempts` | IClientOptions | `10` | Maximum automatic reconnect attempts after unexpected disconnect; set `0` for unlimited attempts | | `mediaPermissionsRecovery` | IClientOptions | — | Auto-recover media permissions for inbound calls | | `clearReconnectToken()` | TelnyxRTC method | — | Prevent auto-reconnection on disconnect | --- ## See Also - [TelnyxRTC Class](/development/webrtc/js-sdk/classes/telnyxrtc) — Client configuration and methods - [IClientOptions](/development/webrtc/js-sdk/interfaces/iclientoptions) — Full configuration reference - [Error Handling](/development/webrtc/js-sdk/how-to/error-handling) — Error codes including reconnection errors - [Best Practices](/development/webrtc/js-sdk/how-to/production-best-practices#reconnection--recovery) — Production reconnection guidance --- ### Framework Integration > Source: https://developers.telnyx.com/development/webrtc/js-sdk/how-to/integrate-with-frameworks.md # Framework Integration The Telnyx WebRTC JS SDK works with any JavaScript framework. This guide covers integration patterns for popular frameworks. --- ## React ### Install ```bash npm install @telnyx/webrtc @telnyx/react-client ``` ### Using the React wrapper The `@telnyx/react-client` package provides hooks and context providers: ```jsx import { TelnyxClientProvider, useCall, useTelnyxClient } from '@telnyx/react-client'; function App() { return ( ); } function CallScreen() { const { client } = useTelnyxClient(); const { call, makeCall, hangup } = useCall(); return ( {call?.state === 'active' && Call in progress} makeCall('+12345678900')}>Call {call && Hang up} ); } ``` ### Using the SDK directly If you prefer to use the SDK without the React wrapper: ```jsx import { useEffect, useRef, useState } from 'react'; import { TelnyxRTC } from '@telnyx/webrtc'; function useTelnyxRTC(token) { const clientRef = useRef(null); const [calls, setCalls] = useState([]); useEffect(() => { const client = new TelnyxRTC({ login_token: token }); client.on('telnyx.ready', () => { console.log('Connected'); }); client.on('telnyx.notification', (notification) => { if (notification.type === 'callUpdate') { setCalls([...client.calls]); } }); client.connect(); clientRef.current = client; return () => { client.disconnect(); }; }, [token]); return { client: clientRef.current, calls }; } ``` Always disconnect the client on component unmount to prevent zombie WebSocket connections. --- ## Next.js Next.js requires special handling because the SDK uses browser APIs (`WebSocket`, `RTCPeerConnection`) that don't exist on the server. ### Dynamic import ```jsx import dynamic from 'next/dynamic'; const CallScreen = dynamic(() => import('../components/CallScreen'), { ssr: false, // Required — SDK uses browser APIs }); export default function Page() { return ; } ``` ### Client component (App Router) ```jsx 'use client'; import { useEffect, useState } from 'react'; export default function CallScreen() { const [client, setClient] = useState(null); useEffect(() => { // Dynamic import to avoid SSR import('@telnyx/webrtc').then(({ TelnyxRTC }) => { const rtc = new TelnyxRTC({ login_token: getToken() }); rtc.connect(); setClient(rtc); }); return () => { client?.disconnect(); }; }, []); return Call UI here; } ``` --- ## Vue ### Composable ```javascript // composables/useTelnyx.js import { ref, onUnmounted } from 'vue'; import { TelnyxRTC } from '@telnyx/webrtc'; export function useTelnyx(token) { const client = ref(null); const calls = ref([]); const connected = ref(false); function connect() { const rtc = new TelnyxRTC({ login_token: token }); rtc.on('telnyx.ready', () => { connected.value = true; }); rtc.on('telnyx.notification', (notification) => { if (notification.type === 'callUpdate') { calls.value = [...rtc.calls]; } }); rtc.connect(); client.value = rtc; } function disconnect() { client.value?.disconnect(); connected.value = false; } onUnmounted(() => { disconnect(); }); return { client, calls, connected, connect, disconnect }; } ``` ### Component ```vue Call Disconnect import { useTelnyx } from '../composables/useTelnyx'; const { client, calls, connected, connect, disconnect } = useTelnyx(token); function makeCall() { client.value?.newCall({ destinationNumber: '+12345678900', audio: true, }); } connect(); ``` --- ## Angular ### Service ```typescript import { Injectable, OnDestroy } from '@angular/core'; import { TelnyxRTC } from '@telnyx/webrtc'; @Injectable({ providedIn: 'root' }) export class TelnyxService implements OnDestroy { private client: TelnyxRTC | null = null; connect(token: string) { this.client = new TelnyxRTC({ login_token: token }); this.client.on('telnyx.ready', () => { console.log('Connected'); }); this.client.on('telnyx.notification', (notification) => { // Handle notifications }); this.client.connect(); } makeCall(destination: string) { return this.client?.newCall({ destinationNumber: destination, audio: true, }); } disconnect() { this.client?.disconnect(); this.client = null; } ngOnDestroy() { this.disconnect(); } } ``` --- ## General Patterns ### Token fetching All frameworks should fetch JWT tokens from a backend endpoint: ```javascript // Don't hardcode tokens const token = await fetch('/api/telnyx-token').then(r => r.text()); ``` ### Audio element management The SDK auto-creates audio elements, but in some frameworks you may want to manage them yourself: ```javascript const call = client.newCall({ destinationNumber: '+12345678900', audio: true, remoteElement: document.getElementById('remoteAudio'), localElement: document.getElementById('localAudio'), }); ``` Or after the call is active: ```javascript // The SDK appends audio elements to document.body by default // You can move them: const audioEl = document.querySelector('audio[src^="blob:"]'); if (audioEl) { document.getElementById('audioContainer').appendChild(audioEl); } ``` ### Cleanup Always clean up on unmount/unload: ```javascript // React useEffect(() => { return () => client?.disconnect(); }, []); // Vue onUnmounted(() => client?.disconnect()); // Angular ngOnDestroy() { this.client?.disconnect(); } // Vanilla window.addEventListener('beforeunload', () => client?.disconnect()); ``` --- ## See Also - [Quickstart](/development/webrtc/js-sdk/quickstart) — Get started in 5 minutes - [Authentication](/development/webrtc/js-sdk/how-to/authenticating-your-app) — JWT setup for production - [IClientOptions](/development/webrtc/js-sdk/interfaces/iclientoptions) — Client configuration - [Best Practices](/development/webrtc/js-sdk/how-to/production-best-practices) — Production deployment guide - [Demo App](https://github.com/team-telnyx/webrtc-demo-js) — Full React reference application --- ### Debug Data & Call Quality Analysis > Source: https://developers.telnyx.com/development/webrtc/js-sdk/how-to/debug-call-issues.md # Debug Data & Call Quality Analysis When calls have quality issues, the Telnyx WebRTC JS SDK provides multiple tools to diagnose the problem. This guide covers collecting debug data, interpreting results, and common troubleshooting patterns. --- ## Data Collection Methods | Method | When to Use | Data Available | |--------|------------|----------------| | **Call Reports** | Production monitoring | RTT, jitter, packet loss, ICE state, audio levels | | **Debug Reports** | Deep troubleshooting | Full WebRTC stats, SDP, ICE candidates, timestamps | | **Console Debug** | Development | SDK internal logs, WebSocket messages | | **Debug Visualizer** | Visual analysis | Charts of call quality over time | --- ## Method 1: Call Reports (Production) Enable call reports for production quality monitoring: ```javascript const client = new TelnyxRTC({ login_token: jwt, enableCallReports: true, callReportInterval: 5000, }); ``` See [Monitor Call Quality](/development/webrtc/js-sdk/how-to/monitor-call-quality) for the full guide. --- ## Method 2: Debug Reports (Deep Troubleshooting) Enable debug output for detailed troubleshooting data. Use `debug: true` with `debugOutput` to control where the data goes: ```javascript const client = new TelnyxRTC({ login_token: jwt, enableCallReports: true, // Collect call stats debug: true, // Enable debug mode debugOutput: 'file', // Write debug data to a file }); ``` Debug data includes: - Full ICE candidate list with timestamps - DTLS handshake state - SDP offer/answer with codec negotiation - Packet-level stats (bytes, packets, loss per direction) - Audio level measurements ### Accessing debug data Call report data is available via the Call Report Stats API after the call ends: ```bash curl "http://voice-sdk-call-report-stats.query.prod.telnyx.io:4000/api/v1/calls/{user_id}/{call_id}" ``` ### Interpreting debug data **Key sections to check:** | Section | What to Look For | |---------|-----------------| | `ice_data.transport` | DTLS state, ICE state, SRTP cipher | | `ice_data.selected_pair` | Which candidate pair is in use (host/srflx/relay) | | `ice_data.candidates` | All gathered candidates with timestamps | | `segments` | Periodic RTT, jitter, packet loss measurements | --- ## Method 3: Console Debug (Development) Enable `debug: true` to get verbose SDK logging in the browser console: ```javascript const client = new TelnyxRTC({ login_token: jwt, debug: true, }); ``` This outputs SDK internal logs (WebSocket messages, ICE events, signaling) to the browser console. No additional configuration needed — `debug: true` enables console logging by default. --- ## Method 4: Debug Visualizer Send debug output to the Telnyx debug visualizer for graphical analysis: ```javascript const client = new TelnyxRTC({ login_token: jwt, debug: true, debugOutput: 'socket', // Send to visualizer }); ``` Open **https://webrtc-debug.telnyx.com/** in another tab to see the live visualization. The visualizer shows: - Call timeline with state transitions - ICE candidate gathering progress - DTLS handshake status - Audio quality graphs (RTT, jitter, packet loss) - Media flow direction --- ## Common Issues & Diagnosis ### One-Way Audio **Check:** 1. Is DTLS connected? → `ice_data.transport.dtls_state === "connected"` 2. Is audio being sent? → Check `bytesSent` in stats 3. Is audio being received? → Check `bytesReceived` in stats 4. Which candidate type? → `ice_data.selected_pair` shows host/srflx/relay **Common causes:** - Asymmetric TURN relay (two nominated candidate pairs, one sending and one receiving) - Firewall blocks media in one direction - VPN interferes with ICE candidates **Diagnosis from debug data:** ``` If dtls_state: "connecting" and ice_state: "connected" → DTLS handshake failing. Check for multiple NICs or asymmetric routing. If bytesSent > 0 but bytesReceived = 0 → Audio is being sent but not received. Check remote firewall. If bytesSent = 0 and bytesReceived > 0 → Audio is being received but not sent. Check local microphone permissions. ``` ### Call Doesn't Connect **Check:** 1. WebSocket state → `client.connection.connected` 2. ICE state → `ice_data.transport.ice_state` 3. STUN accessibility → Any `srflx` candidates? **Common causes:** - Firewall blocks `rtc.telnyx.com:443` (signaling) - Firewall blocks `stun.telnyx.com:3478` (STUN) - Firewall blocks `turn.telnyx.com:443` (TURN) - No `relay` candidates and symmetric NAT ### Choppy Audio **Check:** 1. Jitter → `stats.jitter > 50ms` is poor 2. Packet loss → `stats.packetLoss > 3%` is poor 3. RTT → `stats.rtt > 300ms` is poor **Common causes:** - WiFi congestion (high jitter) - Network congestion (high packet loss) - Long routing path (high RTT) - VPN adding latency ### Echo **Common causes:** - Built-in speakers + mic without echo cancellation - Two audio elements playing the same stream - Headset echo cancellation not working **Fix:** - Recommend headphones - Ensure only one audio element is active per call - Check browser echo cancellation settings --- ## Quick Diagnostic Script Run this in the browser console during a problematic call: ```javascript (async () => { const client = window.__telnyxClient; // Your client instance const calls = client.calls; console.log('=== Telnyx Call Diagnostic ==='); console.log(`Active calls: ${calls.length}`); console.log(`Connected: ${client.connection.connected}`); for (const call of calls) { console.log(` Call ${call.id}: state=${call.state}, direction=${call.direction}`); console.log(` Remote: ${call.remotePartyNumber}`); const pc = call.peerConnection; if (pc) { const stats = await pc.getStats(); stats.forEach(report => { if (report.type === 'candidate-pair' && report.nominated) { console.log(` ICE: state=${report.state}, RTT=${report.currentRoundTripTime * 1000}ms`); } if (report.type === 'inbound-rtp' && report.kind === 'audio') { console.log(` Audio In: packets=${report.packetsReceived}, lost=${report.packetsLost}, jitter=${report.jitter}`); } if (report.type === 'outbound-rtp' && report.kind === 'audio') { console.log(` Audio Out: packets=${report.packetsSent}`); } }); } } })(); ``` --- ## See Also - [Call Report Stats](/development/webrtc/js-sdk/call-report-stats) — Full stats API reference - [Error Handling](/development/webrtc/js-sdk/how-to/error-handling) — Error and warning codes - [Network Requirements](/development/webrtc/js-sdk/how-to/configure-network-firewall) — Firewall and connectivity - [IClientOptions](/development/webrtc/js-sdk/interfaces/iclientoptions) — `debug`, `debugOutput` - [Best Practices](/development/webrtc/js-sdk/how-to/production-best-practices#audio-quality) — Quality monitoring --- ### WebRTC JS SDK error handling > Source: https://developers.telnyx.com/development/webrtc/js-sdk/how-to/error-handling.md # Error Handling The SDK exposes error-related behavior through three main channels: | Event | Purpose | Recommended use | |-------|---------|----------------| | `telnyx.error` | Fatal or blocking SDK errors | Show actionable errors, retry, re-authenticate | | `telnyx.warning` | Non-fatal quality, connectivity, and token warnings | Show degraded-state UI, collect telemetry | | `telnyx.notification` | Call lifecycle updates and compatibility notifications | Drive call UI and hangup handling | Use `telnyx.ready` to know when the client is authenticated and the gateway is ready. Do not treat readiness as a notification case. ## What your application should react to For production integrations, handle these events explicitly: | Event | React in UI? | Retry/recover yourself? | Notes | |-------|--------------|-------------------------|-------| | `telnyx.ready` | Yes | No | Connection is authenticated and ready for calls. Hide reconnecting state here. | | `telnyx.error` | Yes | Sometimes | Fatal/blocking errors. Follow the error code guidance below. | | `telnyx.warning` | Yes for call-affecting warnings | Usually no | Degraded but non-fatal. The SDK continues running and often starts automatic recovery. | | `telnyx.notification` with `type: 'callUpdate'` | Yes | No | Source of truth for call states, hangups, SIP cause/causeCode, and recovered calls. | | `telnyx.socket.close` / `telnyx.socket.error` | Optional | No unless `autoReconnect: false` | Useful for telemetry and reconnecting UI; wait for `telnyx.ready` or `RECONNECTION_EXHAUSTED`. | Do not treat every warning as a failed call. In the current SDK, media/signaling recovery warnings are intentionally emitted before the SDK attempts recovery, so your application can show a short degraded/reconnecting state while the SDK handles the recovery path. > **Version note:** The structured error and warning system (`TELNYX_ERROR_CODES`, `telnyx.warning`, `TelnyxError`) was introduced after v2.25.25. If you are on v2.25.25, see the [Error handling in v2.25.25](#error-handling-in-v22525) section below. --- ## Structured Errors (`telnyx.error`) `telnyx.error` is the primary error surface. Listen for it to handle authentication failures, media errors, and connection issues. ### Imports ```javascript import { SwEvent, TelnyxError, TELNYX_ERROR_CODES, isMediaRecoveryErrorEvent, } from '@telnyx/webrtc'; ``` ### Basic example ```javascript client.on(SwEvent.Error, (event) => { if (isMediaRecoveryErrorEvent(event)) { openPermissionsDialog({ deadline: event.retryDeadline, onRetry: () => event.resume(), onCancel: () => event.reject(), }); return; } if (!(event.error instanceof TelnyxError)) { showErrorMessage('An unknown SDK error occurred.'); return; } switch (event.error.code) { case TELNYX_ERROR_CODES.NETWORK_OFFLINE: showErrorMessage('You appear to be offline.'); break; case TELNYX_ERROR_CODES.AUTHENTICATION_REQUIRED: showErrorMessage('Session expired. Please authenticate again.'); break; default: showErrorMessage(event.error.message); } }); ``` ### Media permission recovery When `mediaPermissionsRecovery.enabled` is configured and `getUserMedia()` fails while answering a call, the error event includes `recoverable: true` with `resume()` and `reject()` callbacks: ```javascript const client = new TelnyxRTC({ login_token: jwt, mediaPermissionsRecovery: { enabled: true, timeout: 10000, }, }); client.on(SwEvent.Error, (event) => { if (isMediaRecoveryErrorEvent(event)) { // Show a dialog asking the user to grant microphone permission // event.retryDeadline is the timestamp by which they must act showPermissionDialog({ onGrant: () => event.resume(), onDismiss: () => event.reject(), }); } }); ``` --- ## Error Code Reference Each error below is classified as **fatal** or **recoverable** and includes guidance on what action you should take versus what the SDK handles automatically. ### SDP errors | Code | Name | Fatal? | Customer action | SDK behavior | |------|------|--------|-----------------|--------------| | `40001` | `SDP_CREATE_OFFER_FAILED` | Fatal | Show error to user; retry with `client.newCall()` | Call is not established | | `40002` | `SDP_CREATE_ANSWER_FAILED` | Fatal | Show error to user; the inbound call cannot be answered | Call is rejected | | `40003` | `SDP_SET_LOCAL_DESCRIPTION_FAILED` | Fatal | Show error to user; retry the call | Call setup fails | | `40004` | `SDP_SET_REMOTE_DESCRIPTION_FAILED` | Fatal | Show error to user; retry the call | Call setup fails | | `40005` | `SDP_SEND_FAILED` | Fatal | Show error to user; retry the call | Signaling could not be sent | ### Media errors | Code | Name | Fatal? | Customer action | SDK behavior | |------|------|--------|-----------------|--------------| | `42001` | `MEDIA_MICROPHONE_PERMISSION_DENIED` | Fatal unless `mediaPermissionsRecovery` is enabled | Prompt user to grant microphone permission in browser/OS settings | Call fails; if `mediaPermissionsRecovery.enabled`, a recoverable error is emitted instead | | `42002` | `MEDIA_DEVICE_NOT_FOUND` | Fatal | Check that a microphone is connected and the `deviceId` is valid | Call fails | | `42003` | `MEDIA_GET_USER_MEDIA_FAILED` | Fatal unless `mediaPermissionsRecovery` is enabled | Check browser permissions and device availability; retry | Call fails; if `mediaPermissionsRecovery.enabled`, a recoverable error is emitted instead | ### Call-control errors | Code | Name | Fatal? | Customer action | SDK behavior | |------|------|--------|-----------------|--------------| | `44001` | `HOLD_FAILED` | Non-fatal per call | Retry hold operation | Hold is not applied | | `44002` | `INVALID_CALL_PARAMETERS` | Fatal | Fix call parameters before retrying | Call is not established | | `44003` | `BYE_SEND_FAILED` | Non-fatal | Call is still hung up locally; no action needed | Local hangup completes but BYE signal may not reach the server | | `44004` | `SUBSCRIBE_FAILED` | Fatal | Check connection state; may need to reconnect | Cannot subscribe to call events | | `44005` | `PEER_CLOSED_DURING_INIT` | Fatal | Retry the call | Peer connection closed before call setup completed | ### ICE restart errors | Code | Name | Fatal? | Customer action | SDK behavior | |------|------|--------|-----------------|--------------| | `47001` | `ICE_RESTART_FAILED` | Fatal for the call | Show call failed/disconnected state; ask the user to retry the call or change networks | Media recovery could not complete | ### WebSocket and transport errors | Code | Name | Fatal? | Customer action | SDK behavior | |------|------|--------|-----------------|--------------| | `45001` | `WEBSOCKET_CONNECTION_FAILED` | Fatal for session | Check network connectivity; call `client.connect()` to retry | Session is not established | | `45002` | `WEBSOCKET_ERROR` | Fatal for session | Show reconnecting UI; SDK auto-reconnects by default (`autoReconnect` is enabled by default). If you disabled `autoReconnect`, call `client.connect()` manually. Wait for `telnyx.ready` to confirm recovery | WebSocket error occurred; SDK schedules `connect()` after a random 2-6 second delay when `autoReconnect` is not disabled | | `45003` | `RECONNECTION_EXHAUSTED` | Fatal for session | All automatic reconnect attempts exhausted. Call `client.disconnect()` then `client.connect()` to start a fresh connection, or recreate the client instance | Automatic reconnect stopped after `maxReconnectAttempts` attempts; default is 10 attempts, set `maxReconnectAttempts: 0` for unlimited attempts | | `45004` | `GATEWAY_FAILED` | Fatal for session | Show reconnecting UI; SDK auto-reconnects by default (`autoReconnect` is enabled by default). If you disabled `autoReconnect`, call `client.connect()` manually. Wait for `telnyx.ready` to confirm recovery | Gateway reported `FAILED` or `FAIL_WAIT`; SDK continues retrying when `autoReconnect` is not disabled | > **`autoReconnect` is enabled by default.** Unless you explicitly set `autoReconnect: false`, the SDK handles reconnection automatically for `WEBSOCKET_ERROR`, `GATEWAY_FAILED`, and signaling-health recovery. You only need to call `client.connect()` manually if you disabled `autoReconnect` or after `RECONNECTION_EXHAUSTED`. ### Authentication and session errors | Code | Name | Fatal? | Customer action | SDK behavior | |------|------|--------|-----------------|--------------| | `46001` | `LOGIN_FAILED` | Fatal for session | Re-authenticate using `client.login()` without recreating the instance | Registration never reached ready state | | `46002` | `INVALID_CREDENTIALS` | Fatal for session | Fix credential parameters; re-authenticate using `client.login()` | Login was rejected before request was sent | | `46003` | `AUTHENTICATION_REQUIRED` | Fatal for session | Re-authenticate using `client.login({ creds: { login_token: newToken } })` without recreating the instance | Request was sent before auth completed or after auth was lost | | `48001` | `NETWORK_OFFLINE` | Fatal for session | Restore network connectivity; SDK auto-reconnects when back online | Browser `offline` event fired | | `49001` | `UNEXPECTED_ERROR` | Fatal | Check logs for details; retry the operation | Unclassified failure during peer/call setup | > **Re-authenticate without recreating the instance.** For `LOGIN_FAILED` (`46001`), `INVALID_CREDENTIALS` (`46002`), and `AUTHENTICATION_REQUIRED` (`46003`), use `client.login()` to re-authenticate on the existing connection: > > ```javascript > // Refresh token without recreating TelnyxRTC > await client.login({ creds: { login_token: newToken } }); > ``` --- ## Structured Warnings (`telnyx.warning`) Warnings are **never fatal**. They describe degraded behavior, quality issues, or situations that may need user action before the session breaks. The SDK continues operating after emitting a warning. ### Basic example ```javascript import { SwEvent, TELNYX_WARNING_CODES } from '@telnyx/webrtc'; client.on(SwEvent.Warning, ({ warning, callId }) => { if (warning.code === TELNYX_WARNING_CODES.TOKEN_EXPIRING_SOON) { refreshToken(); return; } if (warning.code === TELNYX_WARNING_CODES.PEER_CONNECTION_FAILED) { showWarningBanner('Call is reconnecting'); return; } console.warn(`[${warning.code}] ${warning.name}: ${warning.message}`); }); ``` ### Warning event payload Every warning event includes a structured `warning` object and the SDK `sessionId`. When a warning is associated with a specific call, `callId` is also included. Recovery-related warnings may include `reason` and `source` fields for diagnostics: ```javascript client.on(SwEvent.Warning, (event) => { const { warning, sessionId, callId, reason, source } = event; logSdkWarning({ code: warning.code, name: warning.name, message: warning.message, causes: warning.causes, solutions: warning.solutions, sessionId, callId, reason, source, }); }); ``` Use `warning.code` for application logic. Use `warning.message`, `warning.causes`, and `warning.solutions` for support tooling or user-facing troubleshooting copy. ### Warning code reference #### Network quality warnings | Code | Name | Auto-recovered? | Customer action | |------|------|-----------------|-----------------| | `31001` | `HIGH_RTT` | May self-resolve | Show quality indicator; no immediate action needed | | `31002` | `HIGH_JITTER` | May self-resolve | Show quality indicator; no immediate action needed | | `31003` | `HIGH_PACKET_LOSS` | May self-resolve | Show quality indicator; no immediate action needed | | `31004` | `LOW_MOS` | May self-resolve | Show quality indicator; consider advising user | | `31005` | `LOW_LOCAL_AUDIO` | May self-resolve | Show microphone-level indicator; ask the user to check mute/input gain or selected microphone | #### Data-flow warnings | Code | Name | Auto-recovered? | Customer action | |------|------|-----------------|-----------------| | `32001` | `LOW_BYTES_RECEIVED` | May self-resolve on reconnect | Check remote party; show degraded audio indicator | | `32002` | `LOW_BYTES_SENT` | May self-resolve on reconnect | Check local microphone; show degraded audio indicator | #### Connectivity warnings | Code | Name | Auto-recovered? | Customer action | |------|------|-----------------|-----------------| | `33001` | `ICE_CONNECTIVITY_LOST` | SDK attempts ICE reconnect | Show reconnecting indicator; wait for recovery or `PEER_CONNECTION_FAILED` | | `33002` | `ICE_GATHERING_TIMEOUT` | May self-resolve | Check firewall/STUN/TURN config; show warning | | `33003` | `ICE_GATHERING_EMPTY` | No | Check network/firewall settings; STUN/TURN may be blocked | | `33004` | `PEER_CONNECTION_FAILED` | May recover — SDK may attempt ICE restart | Show reconnecting/degraded UI; wait for SDK recovery; only clean up after final hangup or call termination | | `33005` | `ONLY_HOST_ICE_CANDIDATES` | No | Check STUN/TURN config; call may work on local network only | | `33006` | `ANSWER_WHILE_PEER_ACTIVE` | No | Ensure `answer()` is called only once per call; disable the answer button after the first click; check that `answer()` is not invoked from multiple event handlers | | `33007` | `DUPLICATE_INBOUND_ANSWER` | No | Keep a single active `TelnyxRTC` instance for inbound calls; disconnect old clients before replacing them; answer only one duplicate inbound notification | | `33008` | `ICE_CANDIDATE_PAIR_CHANGED` | Usually yes | Log candidate path changes and monitor quality; frequent changes indicate unstable network, VPN, NAT rebinding, or relay fallback | #### Authentication and session warnings | Code | Name | Auto-recovered? | Customer action | |------|------|-----------------|-----------------| | `34001` | `TOKEN_EXPIRING_SOON` | No, but preventable | Refresh the token before it expires; you have ~120 seconds | | `35001` | `SESSION_NOT_REATTACHED` | No | Active calls were lost after reconnect; clean up call UI | #### Signaling health and recovery warnings | Code | Name | Auto-recovered? | Customer action | |------|------|-----------------|-----------------| | `36001` | `SIGNALING_HEALTH_PROBE_TIMEOUT` | SDK force-closes the WebSocket and reconnects | Show reconnecting/degraded UI; do not place a second call; wait for `telnyx.ready` and `callUpdate` recovery | | `36002` | `SIGNALING_REQUEST_TIMEOUT` | SDK force-closes the WebSocket and reconnects for critical call-control requests | Show reconnecting/degraded UI; collect `reason`/`source` for support | | `36003` | `SIGNALING_RECOVERY_REQUIRED` | SDK reconnects signaling and reattaches active calls | Show short interruption state; wait for call reattach or final hangup | | `36004` | `MEDIA_RECOVERY_REQUIRED` | SDK attempts ICE restart without reconnecting the socket | Show media reconnecting indicator; keep call UI active until recovery or final hangup | Signaling health warnings are emitted when the SDK detects a half-dead WebSocket during an active call. This can happen when the browser still reports the socket as `OPEN`, but no signaling bytes are flowing after a network interface change, VPN change, NAT timeout, or proxy/load-balancer drop. The SDK decides one recovery path: - If signaling is unhealthy, it reconnects the WebSocket and reattaches active calls. - If signaling is healthy but media is unhealthy, it attempts ICE restart. - It does not run both recovery paths at the same time. Your application should keep the current call visible, show a reconnecting/degraded state, and wait for the next `callUpdate`, `telnyx.ready`, warning, or final `hangup` before cleaning up the UI. --- ## Call Termination Data When a call reaches `hangup`, inspect these fields on the `Call` object: | Field | Type | Meaning | |-------|------|---------| | `cause` | `string \| null` | High-level cause (`USER_BUSY`, `CALL_REJECTED`, etc.) | | `causeCode` | `number \| null` | Numeric cause code | | `sipCode` | `number \| null` | SIP response code when available | | `sipReason` | `string \| null` | SIP reason phrase when available | Common causes: | Cause | Meaning | |-------|---------| | `NORMAL_CLEARING` | Expected call completion | | `USER_BUSY` | Remote party was busy | | `CALL_REJECTED` | Remote party rejected the call | | `NO_ANSWER` | Call timed out unanswered | | `UNALLOCATED_NUMBER` | Dialed number is invalid or does not exist | --- ## Socket Events ### `telnyx.socket.close` Delivers the browser `CloseEvent`. During a forced safety cleanup, the SDK emits a synthetic abnormal close with `code: 1006` and `wasClean: false`. Useful close codes: | Code | Meaning | |------|---------| | `1000` | Normal closure | | `1001` | Going away | | `1006` | Abnormal closure | | `1011` | Internal error | ### `telnyx.socket.error` Delivers `{ error: ErrorEvent, sessionId: string }`. Browsers expose very little information for WebSocket errors. The SDK also emits `telnyx.error` with code `45002` (`WEBSOCKET_ERROR`) when `ws.onerror` fires. --- ## Connection State Helpers The browser session exposes WebSocket state helpers on `client.connection`: | Getter | Meaning | |--------|---------| | `client.connection.connected` | WebSocket is in `OPEN` | | `client.connection.isAlive` | `CONNECTING` or `OPEN` | | `client.connection.isDead` | `CLOSING` or `CLOSED` | Example: ```javascript const placeCall = (destinationNumber) => { if (!client.connection.connected) { showErrorMessage('Still connecting to Telnyx. Please try again shortly.'); return; } client.newCall({ destinationNumber }); }; ``` --- ## Reconnection Behavior On `telnyx.socket.close` or `telnyx.socket.error`, the SDK clears subscriptions and resets gateway readiness state. `autoReconnect` is enabled by default; unless you set `autoReconnect: false`, the SDK automatically schedules `connect()` after a randomized 2-6 second delay. Automatic reconnect stops after `maxReconnectAttempts` attempts (default: 10), or runs indefinitely when `maxReconnectAttempts: 0`. ### Gateway retry behavior - **UNREGED / NOREG:** Up to 5 registration retries, each delayed 2-6 seconds randomly. After that, `LOGIN_FAILED` (`46001`). - **FAILED / FAIL_WAIT:** `GATEWAY_FAILED` (`45004`) emitted on first detection. Up to 5 retries with 2-6 second random delay before `RECONNECTION_EXHAUSTED` (`45003`). ### Keeping media alive If `keepConnectionAliveOnSocketClose` is `true`, the SDK preserves active peer connections while signaling reconnects. Recovery can create a new `Call` object with `recoveredCallId`. ### Clearing reconnect stickiness By default, the SDK reconnects to the same `b2bua-rtc` instance. To break this stickiness and route to a different instance: ```javascript // Before reconnecting client.clearReconnectToken(); // Or configure the SDK to skip the last voice SDK ID on reconnect const client = new TelnyxRTC({ login_token: jwt, skipLastVoiceSdkId: true, }); ``` > **Note:** `clearReconnectToken()` and `skipLastVoiceSdkId` are available in `@telnyx/webrtc@2.26.4`. --- ## Error Handling in v2.25.25 > **Important:** If you are using SDK version `2.25.25`, the error handling architecture is fundamentally different from the current version. This section documents the v2.25.25 error surface. ### What is different in v2.25.25 | Feature | v2.25.25 | v2.26.0+ | |---------|----------|----------| | Structured error codes | Not available | `TELNYX_ERROR_CODES` with numeric codes | | `telnyx.warning` event | Not available | Available with `TELNYX_WARNING_CODES` | | `TelnyxError` class | Not available | Structured error class with `.code`, `.name`, `.message` | | `isMediaRecoveryErrorEvent()` | Not available | Available for media permission recovery | | `SDK_ERRORS` / `SDK_WARNINGS` | Not available | Available for error/warning metadata | | Primary error surface | `telnyx.error` (raw `Error`) + `telnyx.notification` | `telnyx.error` (structured) + `telnyx.warning` | ### Error events in v2.25.25 In v2.25.25, errors are emitted through `telnyx.error` and `telnyx.notification`: **`telnyx.error`** — Session-level errors with raw `Error` objects (no `.code` property): ```javascript client.on(SwEvent.Error, (event) => { // event.error is a plain Error object — no structured code // event.type may include ERROR_TYPE.invalidCredentialsOptions // event.sessionId is available console.error('SDK error:', event.error?.message || event.error); }); ``` **`telnyx.notification`** — Carries both call lifecycle updates **and** error information. This is the only recommended way to handle media, peer connection, and signaling errors in v2.25.25. Do not listen for `telnyx.rtc.mediaError`, `telnyx.rtc.peerConnectionFailureError`, or `telnyx.rtc.peerConnectionSignalingStateClosed` directly — those are internal events. Use `telnyx.notification` instead: ```javascript client.on(SwEvent.Notification, (notification) => { switch (notification.type) { case 'userMediaError': // notification.error — raw browser Error/DOMException // notification.errorName — error name string // notification.errorMessage — error message string // notification.call — the Call object (if available) // SDK automatically hangs up the call showPermissionPrompt(notification.errorMessage); break; case 'peerConnectionFailureError': // notification.error — raw error // Peer connection failed, but the call may be recovered by the server // via attach with 'recovering' state. Show degraded UI and wait. showReconnectingBanner(); break; case 'signalingStateClosed': // Peer signaling state closed — peer is not recoverable // But the call may still recover through server attach // Only clean up after a final hangup/callUpdate confirms loss showReconnectingBanner(); break; case 'callUpdate': // Normal call lifecycle handleCallUpdate(notification.call); break; } }); ``` Error-related notification types: | `notification.type` | Meaning | Fatal? | Customer action | |---------------------|---------|--------|-----------------| | `userMediaError` | Media device access failed | Yes (for the call) | Prompt user for microphone permission; the SDK hangs up the call automatically | | `peerConnectionFailureError` | Peer connection failed | Peer connection not recoverable, but call may be recovered | Show reconnecting/degraded UI; the call may be restored automatically by the server via attach with `recovering` state; only clean up after a final hangup/call update confirms loss | | `signalingStateClosed` | Peer signaling state closed | Peer connection not recoverable, but call may still be recovered by server | Show reconnecting/degraded UI; the call may recover through auto-created recovering call with the same `call_id`; only clean up after a final hangup/call update confirms loss | ### Authentication errors in v2.25.25 Login errors are emitted on `telnyx.error` with a `type` field for invalid credentials. You can re-authenticate using `client.login()` without recreating the `TelnyxRTC` instance: ```javascript import { SwEvent, ERROR_TYPE } from '@telnyx/webrtc'; client.on(SwEvent.Error, (event) => { if (event.type === ERROR_TYPE.invalidCredentialsOptions) { // Credentials were invalid before the request was sent showLoginError('Please check your credentials.'); return; } // For LOGIN_FAILED or AUTHENTICATION_REQUIRED, re-login without recreating the instance: // await client.login({ creds: { login_token: newToken } }); // Other errors — generic handling showErrorMessage(event.error?.message || 'An error occurred'); }); ``` ### Reconnection in v2.25.25 Reconnection behavior is the same as the current version with these differences: - `autoReconnect` is enabled by default; the SDK automatically reconnects unless you set `autoReconnect: false` - No `maxReconnectAttempts` option (current SDK defaults to 10 automatic reconnect attempts and supports `maxReconnectAttempts: 0` for unlimited attempts) - No `clearReconnectToken()` method - No `skipLastVoiceSdkId` option - `keepConnectionAliveOnSocketClose` is available ### Migrating from v2.25.25 to the latest If you are upgrading from v2.25.25 to the latest version: 1. **Replace `telnyx.notification` error handling** — use `telnyx.error` for fatal errors and `telnyx.warning` for non-fatal conditions. Keep `telnyx.notification` for call lifecycle only. 2. **Replace `notification.type === 'userMediaError'` handling** with `telnyx.error` listener switching on `event.error.code` (`42001`, `42002`, `42003`). 3. **Replace `notification.type === 'peerConnectionFailureError'` handling** with `telnyx.warning` listener for `PEER_CONNECTION_FAILED` (`33004`). 4. **Replace `notification.type === 'signalingStateClosed'` handling** with `telnyx.warning` listener for the appropriate warning code. 5. **Replace `ERROR_TYPE.invalidCredentialsOptions` checks** with `event.error.code === TELNYX_ERROR_CODES.INVALID_CREDENTIALS` (`46002`). Use `client.login()` to re-authenticate without recreating the `TelnyxRTC` instance. 6. **Import new symbols:** `TelnyxError`, `TELNYX_ERROR_CODES`, `TELNYX_WARNING_CODES`, `isMediaRecoveryErrorEvent`. 7. **If you need media permission recovery for inbound calls**, enable `mediaPermissionsRecovery` and handle `isMediaRecoveryErrorEvent(event)`. 8. **Treat `telnyx.ready` as the only readiness signal.** The `vertoClientReady` notification type is no longer emitted on `telnyx.notification`. The legacy RTC events (`telnyx.rtc.mediaError`, `telnyx.rtc.peerConnectionFailureError`, `telnyx.rtc.peerConnectionSignalingStateClosed`) are still emitted for backward compatibility but should not be used for new integrations. --- ## See Also - [Call Class](/development/webrtc/js-sdk/reference/call) — Call control methods - [Handle Reconnection](/development/webrtc/js-sdk/how-to/handle-reconnection) — Reconnection guide - [Monitor Call Quality](/development/webrtc/js-sdk/how-to/monitor-call-quality) — Quality monitoring - [Server Events](/development/webrtc/js-sdk/reference/sw-events) — Low-level signaling events --- ### Production Best Practices > Source: https://developers.telnyx.com/development/webrtc/js-sdk/how-to/production-best-practices.md # Production Best Practices Going from "it works on my machine" to "it works for all users, reliably" requires addressing security, reliability, performance, and monitoring. This guide covers the key areas. --- ## Authentication ### Use JWT in production ```javascript // Production const client = new TelnyxRTC({ login_token: jwt }); // Development only const client = new TelnyxRTC({ login: 'user', password: 'pass' }); ``` JWTs are time-limited, revocable, and don't expose passwords. See [Authenticating Your App](/development/webrtc/js-sdk/how-to/authenticating-your-app). ### Generate JWTs on your backend ```javascript // Backend generates token — API key never reaches the browser app.post('/api/telnyx-token', async (req, res) => { const token = await telnyx.telephonyCredentials.createToken(credentialId); res.json({ token }); }); // Never do this — API key in browser source const response = await fetch('https://api.telnyx.com/v2/telnyx_rtc/access_tokens', { headers: { Authorization: `Bearer ${API_KEY}` }, // API_KEY exposed! }); ``` ### Handle token refresh ```javascript client.on('telnyx.notification', (notification) => { if (notification.type === 'userMediaError') return; // Check for token expiring soon if (notification.type === 'callUpdate' && notification.call?.state === 'destroyed') { // If disconnected due to auth, try refresh } }); // Or use the session event client.on('telnyx.ready', () => { console.log('Connected and authenticated'); }); ``` ### One credential per user Never share a Telephony Credential across multiple users. Each user must have their own JWT to ensure they receive their own incoming calls. --- ## Connection Management ### One client instance per tab ```javascript // Create once let client = null; function getClient() { if (!client) { client = new TelnyxRTC({ login_token: getJwt() }); client.connect(); } return client; } // Creating multiple instances const client1 = new TelnyxRTC({ login_token: jwt }); // WebSocket 1 const client2 = new TelnyxRTC({ login_token: jwt }); // WebSocket 2 — wasteful ``` ### Clean up on page unload ```javascript window.addEventListener('beforeunload', () => { if (client) { client.calls.forEach(call => call.hangup()); client.disconnect(); } }); ``` ### Handle reconnection gracefully ```javascript client.on('telnyx.notification', (notification) => { if (notification.type === 'callUpdate') { const call = notification.call; if (call.state === 'reconnecting') { showBanner('Connection lost. Reconnecting...'); } else if (call.state === 'active' && wasReconnecting) { hideBanner(); } } }); ``` See [Handle Reconnection](/development/webrtc/js-sdk/how-to/handle-reconnection) for the full guide. --- ## Audio Quality ### Request microphone with constraints ```javascript const call = client.newCall({ destinationNumber: '+12345678900', audio: true, // SDK handles getUserMedia internally }); ``` If you need to control the microphone before making a call: ```javascript const stream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true, }, }); ``` ### Monitor call quality Enable call reports in production: ```javascript const client = new TelnyxRTC({ login_token: jwt, enableCallReports: true, callReportInterval: 5000, }); ``` Set up quality alerts: ```javascript client.on('telnyx.notification', (notification) => { if (notification.type === 'callQuality') { const { mos, rtt, jitter, packetLoss } = notification.callQuality; if (mos < 3.0 || rtt > 500 || jitter > 100 || packetLoss > 5) { logQualityIssue(notification); } } }); ``` ### Recommend headphones for agents Built-in speakers + microphone create echo. For call center agents, recommend USB headsets or enforce echo cancellation. --- ## Network Configuration ### Allowlist Telnyx domains Ensure your firewall allows: | Domain | Port | Protocol | Purpose | |--------|------|----------|---------| | `rtc.telnyx.com` | 443 | WebSocket (TLS) | Signaling | | `stun.telnyx.com` | 3478 | UDP | STUN (ICE) | | `turn.telnyx.com` | 3478 | UDP | TURN relay | | `turn.telnyx.com` | 443 | TCP | TURN fallback | | `api.telnyx.com` | 443 | HTTPS | REST API | ### Don't force relay unless necessary ```javascript // Only if you have a specific security requirement const client = new TelnyxRTC({ login_token: jwt, forceRelayCandidate: true, // Forces all media through TURN }); // Default — lets ICE find the best path const client = new TelnyxRTC({ login_token: jwt, }); ``` Forcing relay adds 20-80ms latency per direction. Use it only when corporate policy requires all media to go through a relay. See [Configure Network & Firewall](/development/webrtc/js-sdk/how-to/configure-network-firewall) for the full guide. --- ## Error Handling ### Always handle errors ```javascript client.on('telnyx.notification', (notification) => { if (notification.type === 'userMediaError') { const { code, message } = notification.error; switch (code) { case 1: // Not allowed showError('Microphone access denied. Please allow access in browser settings.'); break; case 2: // Not found showError('No microphone detected. Please connect a microphone.'); break; case 3: // Not readable showError('Microphone in use by another application.'); break; } } }); ``` ### Handle connection failures ```javascript client.on('telnyx.socket.close', () => { showError('Connection to Telnyx lost. Attempting to reconnect...'); }); client.on('telnyx.socket.error', (error) => { logError('WebSocket error', error); }); ``` ### Don't show raw errors to users ```javascript // Technical error exposed to user showError(`Call failed: ${error.message}`); // User-friendly message showError('Unable to connect the call. Please try again.'); ``` --- ## Memory Management ### Clean up call references ```javascript client.on('telnyx.notification', (notification) => { if (notification.call?.state === 'destroyed') { // Remove call from your state removeCallFromState(notification.call.id); } }); ``` ### Remove event listeners ```javascript // When component unmounts (React example) useEffect(() => { const handler = (notification) => { /* ... */ }; client.on('telnyx.notification', handler); return () => { client.off('telnyx.notification', handler); }; }, []); ``` --- ## Monitoring & Observability ### Enable call reports ```javascript const client = new TelnyxRTC({ login_token: jwt, enableCallReports: true, // Auto-upload reports after each call callReportInterval: 5000, // Stats every 5 seconds }); ``` ### Track key metrics | Metric | Good | Warning | Critical | |--------|------|---------|----------| | MOS | > 4.0 | 3.0–4.0 | < 3.0 | | RTT | < 150ms | 150–300ms | > 300ms | | Jitter | < 20ms | 20–50ms | > 50ms | | Packet Loss | < 1% | 1–3% | > 3% | ### Log quality issues server-side ```javascript client.on('telnyx.notification', (notification) => { if (notification.type === 'callQuality') { sendToMonitoring({ callId: notification.call.id, mos: notification.callQuality.mos, timestamp: Date.now(), }); } }); ``` --- ## Deployment Checklist | Requirement | Details | |-------------|---------| | Authentication uses JWT | `login_token` in production, not `login`+`password` | | JWT generated on backend | API key never in browser | | Token refresh handles `TOKEN_EXPIRING_SOON` | Call `client.updateToken()` on warning code 34001 | | `beforeunload` disconnects client | Hang up calls and call `client.disconnect()` | | `enableCallReports: true` | Automatic call reports for production monitoring | | Error handling covers key events | `userMediaError`, `socket.close`, `socket.error` | | Firewall allows Telnyx domains | `rtc.telnyx.com:443`, `stun.telnyx.com:3478`, `turn.telnyx.com:443` | | Reconnection UI shown during `reconnecting` state | Users should see connection status | | Call references cleaned up on `destroyed` state | Prevent memory leaks | | No `forceRelayCandidate: true` unless required | Forced relay adds 20-80ms latency | | Quality metrics logged to monitoring | Track MOS, RTT, jitter, packet loss | | User-friendly error messages | No raw errors shown to users | --- ## See Also - [Authenticating Your App](/development/webrtc/js-sdk/how-to/authenticating-your-app) - [Configure Network & Firewall](/development/webrtc/js-sdk/how-to/configure-network-firewall) - [Handle Reconnection](/development/webrtc/js-sdk/how-to/handle-reconnection) - [Monitor Call Quality](/development/webrtc/js-sdk/how-to/monitor-call-quality) - [Debug Call Issues](/development/webrtc/js-sdk/how-to/debug-call-issues) - [Error Handling](/development/webrtc/js-sdk/how-to/error-handling) --- ### TelnyxRTC Class > Source: https://developers.telnyx.com/development/webrtc/js-sdk/reference/telnyxrtc.md # TelnyxRTC The `TelnyxRTC` class is the main entry point for the Telnyx WebRTC JS SDK. It manages the WebSocket connection to Telnyx's signaling server and provides methods to create calls, handle events, and control the client lifecycle. ## Constructor ```typescript new TelnyxRTC(options: IClientOptions) ``` Creates a new client instance. Does **not** connect automatically — call `connect()` to establish the WebSocket connection. **Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `options` | [IClientOptions](/development/webrtc/js-sdk/interfaces/iclientoptions) | Yes | Client configuration | **Example:** ```javascript import { TelnyxRTC } from '@telnyx/webrtc'; const client = new TelnyxRTC({ login_token: 'YOUR_JWT_TOKEN', // Optional: see IClientOptions for all options debug: false, enableCallReports: true, }); ``` --- ## Methods ### `connect()` Opens a WebSocket connection to `rtc.telnyx.com` and authenticates using the configured credentials. ```javascript client.connect(); ``` Fires `telnyx.ready` on success or `telnyx.error` on failure. ### `disconnect()` Closes the WebSocket connection and cleans up all active calls. ```javascript client.disconnect(); ``` Always call `disconnect()` when the user leaves your app to avoid zombie WebSocket connections. See [Best Practices → Connection Lifecycle](/development/webrtc/js-sdk/how-to/production-best-practices#connection-lifecycle). ### `newCall(options)` Creates and places a new outbound call. ```javascript const call = client.newCall({ destinationNumber: '+12345678900', audio: true, }); ``` **Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `options` | [ICallOptions](/development/webrtc/js-sdk/interfaces/icalloptions) | Yes | Call configuration | **Returns:** [Call](/development/webrtc/js-sdk/classes/call) See [ICallOptions](/development/webrtc/js-sdk/interfaces/icalloptions) for all available options including custom headers, ICE configuration, and media settings. ### `off(event, handler)` Removes an event listener. ```javascript const onReady = () => { /* ... */ }; client.on('telnyx.ready', onReady); // Later: client.off('telnyx.ready', onReady); ``` ### `removeAllListeners()` Removes all event listeners from the client. ```javascript client.removeAllListeners(); ``` --- ## Properties ### `connection` Provides helpers to check the current connection state. ```javascript client.connection.connecting // true during WebSocket handshake client.connection.connected // true when WebSocket is open and authenticated client.connection.closed // true when WebSocket is closed client.connection.isAlive // true if connection is active ``` | Property | Type | Description | |----------|------|-------------| | `connecting` | `boolean` | True during WebSocket handshake | | `connected` | `boolean` | True when authenticated and ready | | `closed` | `boolean` | True when disconnected | | `isAlive` | `boolean` | True if connection is active | ### `calls` An array of all active [Call](/development/webrtc/js-sdk/classes/call) objects. ```javascript // Check if any calls are active if (client.calls.length > 0) { console.log(`Active calls: ${client.calls.length}`); } // Hang up all calls client.calls.forEach(call => call.hangup()); ``` --- ## Events Register event listeners using `client.on(eventName, handler)`: ```javascript client.on('telnyx.ready', () => { /* ... */ }); client.on('telnyx.error', (error) => { /* ... */ }); ``` ### Connection Events | Event | Payload | Description | |-------|---------|-------------| | `telnyx.ready` | — | Client connected and authenticated. **Wait for this before making calls.** | | `telnyx.error` | `ClientErrorEvent` | Connection or authentication error. See [Error Handling](/development/webrtc/js-sdk/error-handling). | | `telnyx.warning` | `ClientWarningEvent` | Non-fatal warning (e.g., token expiring, quality degradation). See [Warning Codes](/development/webrtc/js-sdk/error-handling#warning-codes). | | `telnyx.socket.close` | `CloseEvent` | WebSocket closed. SDK will attempt reconnection automatically. | | `telnyx.socket.error` | `Event` | WebSocket error. | ### Call Events | Event | Payload | Description | |-------|---------|-------------| | `telnyx.notification` | [INotification](/development/webrtc/js-sdk/interfaces/inotification) | Call state updates, media events, and SDK notifications. This is the **primary event** for handling call lifecycle. | ### Stats Events | Event | Payload | Description | |-------|---------|-------------| | `telnyx.stats.frame` | `StatsFrame` | Periodic WebRTC stats (RTT, jitter, packet loss). Emitted every `callReportInterval` ms. | | `telnyx.stats.report` | `StatsReport` | End-of-call summary report. Emitted when a call ends. | --- ## Typical Usage ```javascript import { TelnyxRTC } from '@telnyx/webrtc'; // 1. Create client with JWT const client = new TelnyxRTC({ login_token: 'YOUR_JWT_TOKEN', enableCallReports: true, }); // 2. Register event handlers BEFORE connecting client.on('telnyx.ready', () => { console.log('Connected! Ready to make calls.'); }); client.on('telnyx.error', (error) => { console.error('Error:', error.code, error.message); }); client.on('telnyx.notification', (notification) => { if (notification.type === 'callUpdate') { const call = notification.call; switch (call.state) { case 'ringing': // Incoming call — show UI break; case 'active': // Call connected break; case 'hangup': // Call ended break; } } }); client.on('telnyx.warning', (warning) => { console.warn('Warning:', warning.code, warning.message); }); // 3. Connect client.connect(); // 4. Make a call (after ready) function makeCall(destination) { if (!client.connection.connected) { console.error('Not connected yet!'); return; } const call = client.newCall({ destinationNumber: destination, audio: true, }); } // 5. Disconnect on cleanup window.addEventListener('beforeunload', () => { client.disconnect(); }); ``` --- ## Reconnection The SDK automatically reconnects when the WebSocket drops. You don't need to handle this manually in most cases. ```mermaid stateDiagram-v2 [*] --> Connecting: connect() Connecting --> Connected: telnyx.ready Connected --> Disconnected: socket.close Disconnected --> Connecting: Auto-reconnect Connected --> Disconnected: disconnect() Disconnected --> [*] ``` For advanced reconnection handling, see [Reconnection & Recovery](/development/webrtc/js-sdk/how-to/handle-reconnection). **Key configuration:** | Option | Default | Description | |--------|---------|-------------| | `keepConnectionAliveOnSocketClose` | `false` | Keep PeerConnection alive during reconnect | | `mediaPermissionsRecovery` | — | Auto-recover media permissions for inbound calls | --- ## See Also - [IClientOptions](/development/webrtc/js-sdk/interfaces/iclientoptions) — Full configuration reference - [Call Class](/development/webrtc/js-sdk/classes/call) — Call control methods - [Error Handling](/development/webrtc/js-sdk/error-handling) — Error and warning codes - [Best Practices](/development/webrtc/js-sdk/how-to/production-best-practices) — Production deployment guide - [Authentication](/development/webrtc/js-sdk/how-to/authenticating-your-app) — JWT generation and token refresh --- ### Call Class > Source: https://developers.telnyx.com/development/webrtc/js-sdk/reference/call.md # Call Class The `Call` object represents a voice call. It's created by `client.newCall()` (outbound) or received via `telnyx.notification` (inbound). --- ## Getting a Call Object ### Outbound call ```javascript const call = client.newCall({ destinationNumber: '+12345678900', audio: true, }); ``` ### Inbound call ```javascript client.on('telnyx.notification', (notification) => { if (notification.type === 'callUpdate' && notification.call.state === 'ringing') { const call = notification.call; call.answer(); } }); ``` --- ## Properties | Property | Type | Description | |----------|------|-------------| | `id` | `string` | Unique call identifier | | `state` | `CallState` | Current call state (see [Call States](#call-states)) | | `direction` | `'inbound' \| 'outbound'` | Call direction | | `remotePartyNumber` | `string` | Remote party's phone number | | `remotePartyName` | `string` | Remote party's display name (if available) | | `localPartyNumber` | `string` | Local party's phone number | | `active` | `boolean` | Whether the call is currently active | | `recoveredCallId` | `string` | Previous call ID if this call was recovered after reconnection | | `peerConnection` | `RTCPeerConnection` | Underlying WebRTC PeerConnection (for advanced use) | --- ## Call States ```mermaid stateDiagram-v2 [*] --> new: newCall() new --> requesting: INVITE sent new --> ringing: INVITE received requesting --> ringing: 180 Ringing ringing --> answering: answer() answering --> active: Media connected ringing --> active: Media connected active --> held: hold() held --> active: unhold() active --> hangup: hangup() or remote BYE ringing --> hangup: reject / cancel requesting --> hangup: cancel / failure active --> recovering: Reconnection recovering --> active: Recovery OK hangup --> destroy: Cleanup destroy --> [*] ``` | State | Description | |-------|-------------| | `new` | Call object created, not yet dialed | | `requesting` | Outbound INVITE sent to server | | `ringing` | Inbound: INVITE received. Outbound: remote ringing | | `answering` | Inbound call being answered (media negotiation) | | `active` | Call connected — media flowing | | `held` | Call on hold | | `hangup` | Call ended (local or remote hangup) | | `destroy` | Call object cleaned up | | `recovering` | Call being recovered after reconnection | --- ## Methods ### `answer()` Answer an incoming call. ```javascript client.on('telnyx.notification', (notification) => { if (notification.type === 'callUpdate' && notification.call.state === 'ringing') { notification.call.answer(); } }); ``` Only call `answer()` when the call state is `ringing`. Calling `answer()` on an already-active call creates a duplicate PeerConnection, which causes one-way audio issues. ### `hangup()` End the call. ```javascript // SDK 2.25.x — synchronous call.hangup(); // SDK 2.26.x — async (returns Promise) await call.hangup(); ``` See [Migration Guide](/development/webrtc/js-sdk/migration-guide) for upgrading from 2.25.x. ### `muteAudio()` / `unmuteAudio()` Toggle the microphone. ```javascript call.muteAudio(); // Mute call.unmuteAudio(); // Unmute ``` ### `hold()` / `unhold()` Put the call on hold or resume it. ```javascript call.hold(); // Put on hold (remote hears hold music) call.unhold(); // Resume ``` ### `dtmf(digit)` Send a DTMF tone (0-9, *, #). ```javascript call.dtmf('1'); call.dtmf('*'); call.dtmf('#'); ``` ### `sendDigits(digits)` Send a sequence of DTMF digits. ```javascript call.sendDigits('1'); ``` --- ## Events Register event listeners using `call.on(eventName, handler)`: ### Call State Events | Event | Payload | Description | |-------|---------|-------------| | `telnyx.notification` | [INotification](/development/webrtc/js-sdk/reference/inotification) | Call state updates, media events | ```javascript const call = client.newCall({ destinationNumber: '+12345678900', audio: true, }); call.on('telnyx.notification', (notification) => { switch (notification.call.state) { case 'active': console.log('Call connected'); break; case 'hangup': console.log('Call ended'); break; } }); ``` --- ## Advanced ### Access the PeerConnection For custom WebRTC monitoring or manipulation: ```javascript const pc = call.peerConnection; // Get current ICE connection state console.log('ICE state:', pc.iceConnectionState); // Get current DTLS state console.log('DTLS state:', pc.connectionState); // Get stats const stats = await pc.getStats(); stats.forEach((report) => { if (report.type === 'candidate-pair' && report.nominated) { console.log('Nominated pair:', report); } }); ``` Direct PeerConnection access is for advanced use cases only. The SDK manages the PeerConnection lifecycle — calling methods like `close()` or `setRemoteDescription()` directly may break the call. ### Custom headers Add SIP headers to the INVITE for server-side correlation: ```javascript const call = client.newCall({ destinationNumber: '+12345678900', audio: true, customHeaders: [ { name: 'X-Call-Session', value: sessionUuid }, { name: 'X-Agent-ID', value: agentId }, ], }); ``` --- ## Common Patterns ### Simple outbound call with state handling ```javascript const call = client.newCall({ destinationNumber: '+12345678900', audio: true, }); call.on('telnyx.notification', (notification) => { switch (notification.call.state) { case 'requesting': showDialingUI(); break; case 'ringing': showRingingUI(); break; case 'active': showActiveCallUI(); break; case 'hangup': cleanupCallUI(); break; } }); // Cancel the call if not yet connected cancelButton.addEventListener('click', () => { call.hangup(); }); ``` ### Inbound call with accept/reject UI ```javascript client.on('telnyx.notification', (notification) => { if (notification.type === 'callUpdate' && notification.call.state === 'ringing') { const call = notification.call; showIncomingCallUI({ from: call.remotePartyNumber, onAccept: () => call.answer(), onReject: () => call.hangup(), }); } }); ``` ### Hold and resume ```javascript // Put call on hold holdButton.addEventListener('click', () => { call.hold(); }); // Resume the call resumeButton.addEventListener('click', () => { call.unhold(); }); ``` --- ## See Also - [ICallOptions](/development/webrtc/js-sdk/reference/icalloptions) — Call configuration options - [INotification](/development/webrtc/js-sdk/reference/inotification) — Notification types and payloads - [TelnyxRTC Class](/development/webrtc/js-sdk/reference/telnyxrtc) — Client methods and events - [SDK Commonalities](/development/webrtc/js-sdk/explanation/call-state-lifecycle) — Call states across all SDK platforms - [Best Practices](/development/webrtc/js-sdk/how-to/production-best-practices) — Production call management guide --- ### IClientOptions > Source: https://developers.telnyx.com/development/webrtc/js-sdk/reference/iclientoptions.md # IClientOptions Options passed to the `TelnyxRTC` constructor to configure the client. --- ## Quick Reference ```javascript import { TelnyxRTC } from '@telnyx/webrtc'; const client = new TelnyxRTC({ login_token: 'YOUR_JWT_TOKEN', // Authentication (required) enableCallReports: true, // Call quality monitoring (default: true) debug: false, // Debug output }); ``` --- ## Authentication Choose one authentication method. See [Authentication](/development/webrtc/js-sdk/how-to/authenticating-your-app) for the full guide. | Property | Type | Required | Description | |----------|------|----------|-------------| | `login_token` | `string` | (if using JWT) | JWT token for authentication. **Recommended for production.** Generate from your backend via `POST /v2/telephony_credentials/{id}/token`. Valid for 24 hours. | | `login` | `string` | (if using credentials) | SIP username (from Telephony Credential). **Development only.** | | `password` | `string` | (if using credentials) | SIP password. **Development only.** | | `anonymous_login` | `object` | (if anonymous) | Connect to an AI assistant without credentials. See [Anonymous Login](#anonymous-login). | **Use `login_token` (JWT) for production applications.** Credentials (`login` + `password`) are long-lived with no automatic rotation. JWTs expire after 24 hours and support refresh. See [Authenticating Your App](/development/webrtc/js-sdk/how-to/authenticating-your-app). **JWT (production):** ```javascript const client = new TelnyxRTC({ login_token: jwt, }); ``` **Credential (development only):** ```javascript const client = new TelnyxRTC({ login: 'gencred...', password: 'your-password', }); ``` ### Anonymous Login Connect to an AI assistant without requiring a credential. The `anonymous_login` option accepts an object with the target configuration: ```javascript const client = new TelnyxRTC({ anonymous_login: { target_type: 'ai_assistant', // Currently the only supported type target_id: 'YOUR_AI_ASSISTANT_ID', // The AI assistant to connect to }, }); ``` | Property | Type | Required | Description | |----------|------|----------|-------------| | `target_type` | `string` | | The target type. Currently only `'ai_assistant'` is supported. | | `target_id` | `string` | | The ID of the AI assistant to connect to. | | `target_version_id` | `string` | — | A specific version of the AI assistant. | | `target_params` | `object` | — | Optional parameters forwarded to the assistant. Known key: `conversation_id` (string) to join an existing conversation. Additional keys are passed through as-is. | **Example — Continue a conversation:** ```javascript const client = new TelnyxRTC({ anonymous_login: { target_type: 'ai_assistant', target_id: 'asst_abc123', target_params: { conversation_id: 'conv_xyz789', // Resume existing conversation }, }, }); ``` --- ## Connection Control WebSocket, reconnection, and region behavior. | Property | Type | Default | Description | |----------|------|---------|-------------| | `env` | `string` | — | Custom signaling server URL. Override `rtc.telnyx.com`. Only use for testing. | | `region` | `string` | — | Region to use for the connection. | | `keepConnectionAliveOnSocketClose` | `boolean` | `false` | Keep PeerConnection alive during WebSocket reconnection. Call media continues flowing while signaling reconnects. | | `skipLastVoiceSdkId` | `boolean` | `false` | When reconnecting with a stored `voice_sdk_id`, route to a different B2BUA-RTC instance instead of sticky-reconnecting to the same one. Useful when retrying after errors caused by stale state on a specific node. | | `rtcIp` | `string` | — | Custom RTC connection IP address. Useful when using a custom signaling server. | | `rtcPort` | `number` | — | Custom RTC connection port. Useful when using a custom signaling server. | | `useCanaryRtcServer` | `boolean` | `false` | Use Telnyx's canary RTC server. | The SDK automatically reconnects when the WebSocket drops. There is no `reconnect` option — reconnection is always automatic. **Enable call recovery:** ```javascript const client = new TelnyxRTC({ login_token: jwt, keepConnectionAliveOnSocketClose: true, }); ``` --- ## ICE & Network Configure STUN/TURN and ICE behavior. | Property | Type | Default | Description | |----------|------|---------|-------------| | `iceServers` | `RTCIceServer[]` | Auto-provisioned | Custom ICE servers. Overrides SDK defaults. Only use if you have custom TURN infrastructure. | | `prefetchIceCandidates` | `boolean` | `true` | Pre-gather ICE candidates before the call is placed. Reduces call setup time. | | `forceRelayCandidate` | `boolean` | `false` | Force all media through TURN relay servers. Hides the client's public IP but adds latency. | | `trickleIce` | `boolean` | — | Enable Trickle ICE. Sends candidates incrementally instead of waiting for full gathering. Faster call setup. | | `mutedMicOnStart` | `boolean` | — | Start with microphone muted by default. | The SDK automatically provisions STUN/TURN servers. You don't need to configure `iceServers` in most cases. See [Network Requirements](/development/webrtc/js-sdk/how-to/configure-network-firewall). **Force TURN for privacy:** ```javascript const client = new TelnyxRTC({ login_token: jwt, forceRelayCandidate: true, // All media through TURN }); ``` **Custom ICE servers:** ```javascript const client = new TelnyxRTC({ login_token: jwt, iceServers: [ { urls: 'stun:stun.custom.com:3478' }, { urls: 'turn:turn.custom.com:443?transport=udp', username: 'myuser', credential: 'mypass', }, ], }); ``` --- ## Audio | Property | Type | Default | Description | |----------|------|---------|-------------| | `ringtoneFile` | `string` | — | URL of a wav/mp3 file to play as the incoming call ringtone. | | `ringbackFile` | `string` | — | URL of a wav/mp3 file to play as ringback tone. Use when you've disabled "Generate Ringback Tone" in your SIP Connection configuration. | **Custom ringtone and ringback:** ```javascript const client = new TelnyxRTC({ login_token: jwt, ringtoneFile: './sounds/incoming_call.mp3', ringbackFile: './sounds/ringback_tone.mp3', }); ``` --- ## Call Reports Enable post-call quality monitoring and real-time stats. | Property | Type | Default | Description | |----------|------|---------|-------------| | `enableCallReports` | `boolean` | `true` | Enable call reports with WebRTC stats (RTT, jitter, packet loss, ICE data). | | `callReportInterval` | `number` | `5000` | Interval in milliseconds between `telnyx.stats.frame` events during calls. | **Call reports are enabled by default.** You can customize the interval: ```javascript const client = new TelnyxRTC({ login_token: jwt, callReportInterval: 3000, // Stats every 3 seconds }); ``` **Listen for stats:** ```javascript client.on('telnyx.stats.frame', (stats) => { console.log('RTT:', stats.rtt, 'Jitter:', stats.jitter); }); client.on('telnyx.stats.report', (report) => { console.log('Call ended. Final report:', report); }); ``` See [Monitor Call Quality](/development/webrtc/js-sdk/how-to/monitor-call-quality) for the full data schema. --- ## Debugging Configure debug output for troubleshooting. | Property | Type | Default | Description | |----------|------|---------|-------------| | `debug` | `boolean` | `false` | Enable debug logging. Outputs SDK internal logs to the browser console. | | `debugOutput` | `'socket' \| 'file'` | — | Where to send debug output. `'socket'` sends to the debug visualizer at `https://webrtc-debug.telnyx.com/`. `'file'` writes debug data to a file. | **Enable console debug logging:** ```javascript const client = new TelnyxRTC({ login_token: jwt, debug: true, }); ``` **Send to debug visualizer:** ```javascript const client = new TelnyxRTC({ login_token: jwt, debug: true, debugOutput: 'socket', // View at https://webrtc-debug.telnyx.com/ }); ``` **Write debug data to a file:** ```javascript const client = new TelnyxRTC({ login_token: jwt, debug: true, debugOutput: 'file', }); ``` See [Debug Call Issues](/development/webrtc/js-sdk/how-to/debug-call-issues) for interpreting debug output. --- ## Media Permissions Recovery Handle microphone permission failures for inbound calls with a recoverable error pattern. | Property | Type | Default | Description | |----------|------|---------|-------------| | `mediaPermissionsRecovery.enabled` | `boolean` | `false` | Enable the recovery flow. | | `mediaPermissionsRecovery.timeout` | `number` | — | Maximum time in ms to wait for the app to call `resume()` or `reject()`. Recommended max: 25000. | | `mediaPermissionsRecovery.onSuccess` | `() => void` | — | Called when retry `getUserMedia` succeeds after `resume()`. | | `mediaPermissionsRecovery.onError` | `(error: Error) => void` | — | Called when retry fails, timeout expires, or app calls `reject()`. | When enabled and `getUserMedia` fails while answering an inbound call, the SDK emits a recoverable `telnyx.error` event with `resume()` and `reject()` callbacks. Your app can prompt the user to fix permissions before the call fails: ```javascript import { TelnyxRTC, isMediaRecoveryErrorEvent } from '@telnyx/webrtc'; const client = new TelnyxRTC({ login_token: jwt, mediaPermissionsRecovery: { enabled: true, timeout: 20000, onSuccess: () => console.log('Media recovered'), onError: (err) => console.error('Recovery failed', err), }, }); client.on('telnyx.error', (event) => { if (isMediaRecoveryErrorEvent(event)) { showPermissionDialog({ onContinue: () => event.resume(), onCancel: () => event.reject?.(), }); } }); ``` --- ## Full Example ```javascript import { TelnyxRTC } from '@telnyx/webrtc'; const client = new TelnyxRTC({ // Authentication login_token: 'YOUR_JWT_TOKEN', // Connection recovery keepConnectionAliveOnSocketClose: true, // Media recovery for inbound calls mediaPermissionsRecovery: { enabled: true, timeout: 20000, }, // Call reports (enabled by default) callReportInterval: 5000, // ICE optimization prefetchIceCandidates: true, trickleIce: true, }); client.on('telnyx.ready', () => { console.log('Connected'); }); client.on('telnyx.error', (error) => { console.error('Error:', error.code, error.message); }); client.connect(); ``` --- ## See Also - [TelnyxRTC Class](/development/webrtc/js-sdk/reference/telnyxrtc) — Client methods and events - [Authenticating Your App](/development/webrtc/js-sdk/how-to/authenticating-your-app) — JWT, credentials, and token refresh - [ICallOptions](/development/webrtc/js-sdk/reference/icalloptions) — Per-call configuration - [Handle Reconnection](/development/webrtc/js-sdk/how-to/handle-reconnection) — Connection recovery - [Network Requirements](/development/webrtc/js-sdk/how-to/configure-network-firewall) — STUN/TURN/firewall - [Production Best Practices](/development/webrtc/js-sdk/how-to/production-best-practices) — Production configuration guide --- ### ICallOptions > Source: https://developers.telnyx.com/development/webrtc/js-sdk/reference/icalloptions.md # ICallOptions Options passed to `client.newCall(options)` to configure call behavior. --- ## Quick Reference ```javascript const call = client.newCall({ destinationNumber: '+12345678900', // Required audio: true, // Required callerName: 'John Doe', // Optional caller ID trickleIce: true, // Faster call setup }); ``` --- ## Required Properties | Property | Type | Description | |----------|------|-------------| | `destinationNumber` | `string` | Phone number or SIP URI to call. Use E.164 format for PSTN (e.g., `+12345678900`) or `sip:user@domain` for SIP. | | `audio` | `boolean` | Enable audio for this call. Always `true` for voice calls. | --- ## Call Identity Customize how the call appears to the remote party. | Property | Type | Default | Description | |----------|------|---------|-------------| | `callerName` | `string` | — | Display name shown to the remote party (Caller ID name) | | `callerNumber` | `string` | — | Phone number shown to the remote party (Caller ID number) | | `customHeaders` | `SipHeader[]` | — | Custom SIP headers to include in the INVITE. Each header has `name` and `value` properties. | **Example — Custom caller ID:** ```javascript const call = client.newCall({ destinationNumber: '+12345678900', audio: true, callerName: 'Acme Corp', callerNumber: '+18005551234', customHeaders: [ { name: 'X-Customer-ID', value: '12345' }, { name: 'X-Agent-Name', value: 'john.doe' }, ], }); ``` --- ## ICE & Network Control how the call establishes media connectivity. | Property | Type | Default | Description | |----------|------|---------|-------------| | `trickleIce` | `boolean` | `true` | Send ICE candidates incrementally instead of waiting for all to gather. **Keep enabled for faster call setup.** | | `prefetchIceCandidates` | `boolean` | `true` | Pre-gather ICE candidates before the call is placed. Reduces call setup time. | | `forceRelayCandidate` | `boolean` | `false` | Force all media through TURN relay servers. Hides the client's public IP. Adds latency. | | `iceServers` | `RTCIceServer[]` | Auto | Custom ICE servers. Overrides the SDK's default STUN/TURN configuration. | **Example — Force TURN for privacy:** ```javascript const call = client.newCall({ destinationNumber: '+12345678900', audio: true, forceRelayCandidate: true, // All media through TURN }); ``` **Example — Custom ICE servers:** ```javascript const call = client.newCall({ destinationNumber: '+12345678900', audio: true, iceServers: [ { urls: 'stun:stun.custom.com:3478' }, { urls: 'turn:turn.custom.com:443', username: 'myuser', credential: 'mypass', }, ], }); ``` The SDK automatically provisions STUN/TURN servers. Only override `iceServers` if you have custom infrastructure. See [Network Requirements](/development/webrtc/js-sdk/how-to/configure-network-firewall). --- ## Media Configuration Control audio devices and streams. | Property | Type | Default | Description | |----------|------|---------|-------------| | `localElement` | `HTMLAudioElement` | Auto-created | HTML element for playing local audio (hearing yourself) | | `remoteElement` | `HTMLAudioElement` | Auto-created | HTML element for playing remote audio (hearing the other party) | | `localStream` | `MediaStream` | — | Custom local media stream. Use to provide a pre-obtained stream. | | `remoteStream` | `MediaStream` | — | Custom remote media stream. | | `preferred_codecs` | `RTCRtpCodecCapability[]` | — | Preferred audio codecs. Defaults to Opus. | | `sdpASBandwidthKbps` | `number` | — | Bandwidth limit in kbps (set in SDP AS attribute) | **Example — Custom audio elements:** ```javascript const remoteAudio = document.getElementById('remoteAudio'); const localAudio = document.getElementById('localAudio'); const call = client.newCall({ destinationNumber: '+12345678900', audio: true, remoteElement: remoteAudio, localElement: localAudio, }); ``` **Example — Pre-obtained media stream:** ```javascript // Get microphone access before placing the call const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); const call = client.newCall({ destinationNumber: '+12345678900', audio: true, localStream: stream, }); ``` --- ## Advanced | Property | Type | Default | Description | |----------|------|---------|-------------| | `sessionId` | `string` | — | Custom session ID for call correlation | | `retryBucketId` | `string` | — | ID for call retry bucket | | `timeoutSecs` | `number` | — | Call setup timeout in seconds | | `telnyxSessionId` | `string` | — | Telnyx session ID (for re-attach scenarios) | | `telnyxCallId` | `string` | — | Telnyx call ID (for re-attach scenarios) | | `isRecovered` | `boolean` | — | Whether this call was recovered after reconnection | --- ## Common Patterns ### Basic voice call ```javascript const call = client.newCall({ destinationNumber: '+12345678900', audio: true, }); ``` ### Call with SIP URI ```javascript const call = client.newCall({ destinationNumber: 'sip:agent@customer.sip.telnyx.com', audio: true, }); ``` ### Call with custom headers (for Call Control correlation) ```javascript const call = client.newCall({ destinationNumber: '+12345678900', audio: true, customHeaders: [ { name: 'X-Call-Session', value: sessionUuid }, ], }); ``` ### Privacy-focused call (force TURN) ```javascript const call = client.newCall({ destinationNumber: '+12345678900', audio: true, forceRelayCandidate: true, }); ``` --- ## See Also - [Call Class](/development/webrtc/js-sdk/classes/call) — Call control methods (answer, hangup, mute, hold) - [IClientOptions](/development/webrtc/js-sdk/interfaces/iclientoptions) — Client-level configuration - [Network Requirements](/development/webrtc/js-sdk/how-to/configure-network-firewall) — ICE/STUN/TURN configuration - [Best Practices](/development/webrtc/js-sdk/how-to/production-best-practices#call-management) — Call management best practices --- ### INotification > Source: https://developers.telnyx.com/development/webrtc/js-sdk/reference/inotification.md # INotification The `INotification` object is emitted via the `telnyx.notification` event. It contains information about call state changes, media events, and SDK notifications. --- ## Properties | Property | Type | Description | |----------|------|-------------| | `type` | `string` | Notification type (see below) | | `call` | [Call](/development/webrtc/js-sdk/classes/call) | The call object this notification relates to (if applicable) | | `timestamp` | `number` | Unix timestamp of the notification | --- ## Notification Types | Type | Description | When It Fires | |------|-------------|---------------| | `callUpdate` | Call state changed | Call rings, connects, hangs up, etc. | | `userMediaError` | Media access failed | Browser denied microphone/camera permissions | | `peerConnectionFailedError` | ICE/DTLS connection failed | Media could not be established | | `signalingStateClosed` | PeerConnection signaling closed | SIP signaling terminated unexpectedly | | `vertoClientReady` | Client is ready | Initial connection established | --- ## Type: `callUpdate` The most common notification type. Fired whenever a call's state changes. ```javascript client.on('telnyx.notification', (notification) => { if (notification.type === 'callUpdate') { const call = notification.call; switch (call.state) { case 'new': // Call object created (before dialing) break; case 'requesting': // Outbound call: INVITE sent to server break; case 'ringing': // Inbound call: received INVITE // Outbound call: remote party is ringing break; case 'answering': // Inbound call: answering in progress break; case 'active': // Call connected — media flowing break; case 'held': // Call on hold break; case 'hangup': // Call ended break; case 'destroy': // Call object cleaned up break; case 'recovering': // Call being recovered after reconnection break; } } }); ``` ### Call State Diagram ```mermaid stateDiagram-v2 [*] --> new: newCall() new --> requesting: INVITE sent (outbound) new --> ringing: INVITE received (inbound) requesting --> ringing: 180 Ringing ringing --> answering: answer() answering --> active: Media connected ringing --> active: Media connected active --> held: hold() held --> active: unhold() active --> hangup: hangup() or remote hangup ringing --> hangup: reject or hangup requesting --> hangup: cancel or failure active --> recovering: Reconnection recovering --> active: Recovery successful hangup --> destroy: Cleanup ``` --- ## Type: `userMediaError` Fired when the browser denies or fails to access media devices (microphone/camera). ```javascript client.on('telnyx.notification', (notification) => { if (notification.type === 'userMediaError') { console.error('Media error:', notification.call.state); // Common causes: // - User denied microphone permission // - No microphone available // - Another app is using the microphone showPermissionErrorUI(); } }); ``` **Common causes:** - User clicked "Block" on the permission prompt - No microphone/camera detected - Another application is using the device - System-level permission denied (OS settings) **Recommended response:** Show a clear message asking the user to grant microphone access, with a link to browser settings. --- ## Type: `peerConnectionFailedError` Fired when the WebRTC PeerConnection fails to establish media. This usually means ICE negotiation or DTLS handshake failed. ```javascript client.on('telnyx.notification', (notification) => { if (notification.type === 'peerConnectionFailedError') { console.error('Peer connection failed'); // Common causes: // - Firewall blocking TURN servers // - Symmetric NAT without TURN // - DTLS fingerprint mismatch showConnectionErrorUI(); } }); ``` **Common causes:** - Firewall blocks UDP to TURN servers - Symmetric NAT prevents direct connectivity - VPN interfering with ICE - Docker/container network issues **Recommended response:** Suggest the user check their network connection. See [Network Requirements](/development/webrtc/js-sdk/how-to/configure-network-firewall). --- ## Type: `signalingStateClosed` Fired when the PeerConnection's signaling state becomes `closed`, indicating the SIP signaling channel has terminated. ```javascript client.on('telnyx.notification', (notification) => { if (notification.type === 'signalingStateClosed') { console.warn('Signaling state closed for call:', notification.call.id); // The call will transition to hangup state shortly } }); ``` This is usually followed by a `callUpdate` with state `hangup`. --- ## Type: `vertoClientReady` Fired when the client has successfully connected and authenticated with the Telnyx signaling server. This is equivalent to the `telnyx.ready` event but delivered as a notification. ```javascript client.on('telnyx.notification', (notification) => { if (notification.type === 'vertoClientReady') { console.log('Client ready for calls'); } }); ``` --- ## Listening to Notifications ### On the Client ```javascript client.on('telnyx.notification', (notification) => { switch (notification.type) { case 'callUpdate': handleCallUpdate(notification.call); break; case 'userMediaError': handleMediaError(notification.call); break; case 'peerConnectionFailedError': handleConnectionError(notification.call); break; } }); ``` ### On a Call You can also listen on individual call objects: ```javascript const call = client.newCall({ destinationNumber: '+12345678900', audio: true, }); call.on('telnyx.notification', (notification) => { // Only notifications for this specific call if (notification.call.state === 'active') { console.log('Call connected!'); } }); ``` Listening on the client gives you notifications for ALL calls. Listening on a specific call gives you notifications for that call only. Choose based on your app's architecture. --- ## See Also - [Call Class](/development/webrtc/js-sdk/classes/call) — Call state and control methods - [TelnyxRTC Class](/development/webrtc/js-sdk/classes/telnyxrtc) — Client-level events - [Error Handling](/development/webrtc/js-sdk/error-handling) — Error and warning codes - [SDK Commonalities](/development/webrtc/js-sdk/explanation/call-state-lifecycle) — Call states across all SDK platforms --- ### Switch/Server Events > Source: https://developers.telnyx.com/development/webrtc/js-sdk/reference/sw-events.md # Switch/Server Events The Telnyx signaling server sends events over the WebSocket connection during the call lifecycle. These are the raw server-side events — most applications should use the higher-level `telnyx.notification` event instead. See [INotification](/development/webrtc/js-sdk/interfaces/inotification) for the recommended approach. **Most developers don't need to handle these events directly.** The SDK translates them into [INotification](/development/webrtc/js-sdk/interfaces/inotification) objects. Use `telnyx.notification` unless you need low-level signaling details. --- ## Event Reference ### Client Lifecycle | Event | Direction | Description | |-------|-----------|-------------| | `telnyx.login` | Client → Server | Authentication request (login + password or login_token) | | `telnyx.login.success` | Server → Client | Authentication successful | | `telnyx.login.error` | Server → Client | Authentication failed (invalid credentials, expired token) | | `telnyx.logout` | Client → Server | Client disconnecting | ### Call Lifecycle | Event | Direction | Description | |-------|-----------|-------------| | `telnyx.invite` | Server → Client | Incoming call (INVITE) | | `telnyx.invite.response` | Client → Server | Response to incoming call (trying, ringing, answer) | | `telnyx.answer` | Client → Server | Accept incoming call | | `telnyx.ringing` | Server → Client | Remote party is ringing (outbound call) | | `telnyx.bridge` | Server → Client | Call bridged (both parties connected) | | `telnyx.bye` | Server → Client | Remote party hung up | | `telnyx.hangup` | Client → Server | Local hangup | ### Media | Event | Direction | Description | |-------|-----------|-------------| | `telnyx.media` | Client → Server | SDP offer/answer exchange | | `telnyx.media.candidate` | Client → Server | ICE candidate (trickle ICE) | ### Call Control | Event | Direction | Description | |-------|-----------|-------------| | `telnyx.hold` | Client → Server | Put call on hold | | `telnyx.unhold` | Client → Server | Take call off hold | | `telnyx.dtmf` | Client → Server | Send DTMF tone | | `telnyx.transfer` | Client → Server | Transfer call | | `telnyx.fax` | Server → Client | Fax detection event | ### Presence & Registration | Event | Direction | Description | |-------|-----------|-------------| | `telnyx.register` | Client → Server | SIP REGISTER (credential-based) | | `telnyx.unregister` | Client → Server | SIP UNREGISTER | | `telnyx.gateway.state` | Server → Client | Gateway connection state (up/down) | --- ## Event Flow: Outbound Call ```mermaid sequenceDiagram participant Client participant Server participant Remote Client->>Server: telnyx.media (SDP offer) Client->>Server: telnyx.media.candidate (ICE candidates) Server->>Remote: INVITE Server->>Client: telnyx.ringing Remote->>Server: 200 OK Server->>Client: telnyx.bridge Note over Client,Remote: Media flowing (active call) Remote->>Server: BYE Server->>Client: telnyx.bye ``` --- ## Event Flow: Inbound Call ```mermaid sequenceDiagram participant Remote participant Server participant Client Remote->>Server: INVITE Server->>Client: telnyx.invite Client->>Server: telnyx.invite.response (trying) Client->>Server: telnyx.answer Client->>Server: telnyx.media (SDP answer) Client->>Server: telnyx.media.candidate (ICE candidates) Server->>Client: telnyx.bridge Note over Client,Remote: Media flowing (active call) Client->>Server: telnyx.hangup Server->>Remote: BYE ``` --- ## Listening to Server Events For advanced use cases, you can listen to raw server events by enabling debug mode and parsing the WebSocket messages: ```javascript const client = new TelnyxRTC({ login_token: jwt, debug: true, debugOutput: 'socket', }); ``` Or intercept WebSocket messages directly: ```javascript // Low-level: intercept raw WebSocket messages const originalSend = client.connection.socket.send.bind(client.connection.socket); client.connection.socket.send = function(data) { const parsed = JSON.parse(data); console.log('→ Server:', parsed.method); return originalSend(data); }; ``` Listening to raw WebSocket messages is **not a supported API**. The message format may change between SDK versions. Use `telnyx.notification` for stable event handling. --- ## Server Events vs INotification | Aspect | Server Events | INotification | |--------|--------------|---------------| | **Level** | Low-level signaling | High-level SDK abstraction | | **Stability** | May change between versions | Stable across versions | | **Payload** | Raw SIP/Verto format | Structured call object | | **Use case** | Debugging, low-level control | Application development | | **Event name** | `telnyx.invite`, `telnyx.bye`, etc. | `telnyx.notification` | **Use `telnyx.notification` (INotification) for application code:** ```javascript // Recommended — high-level, stable client.on('telnyx.notification', (notification) => { if (notification.type === 'callUpdate') { const call = notification.call; // Handle call state changes } }); // Not recommended — low-level, may change // (No public API for this — would require intercepting WebSocket) ``` --- ## Gateway State Events The `telnyx.gateway.state` event indicates when the WebSocket connection to the gateway goes up or down: ```javascript // Listen via notification client.on('telnyx.notification', (notification) => { if (notification.type === 'gatewayStateUpdate') { console.log('Gateway state:', notification.state); // 'up' = connected, 'down' = disconnected } }); ``` **Gateway DOWN** can indicate: - Network interruption - Server-side maintenance - Credential revoked - WebSocket timeout The SDK automatically attempts reconnection. See [Reconnection & Recovery](/development/webrtc/js-sdk/how-to/handle-reconnection). --- ## See Also - [INotification](/development/webrtc/js-sdk/interfaces/inotification) — High-level notification types (recommended) - [Call Class](/development/webrtc/js-sdk/classes/call) — Call state and control methods - [TelnyxRTC Class](/development/webrtc/js-sdk/classes/telnyxrtc) — Client events - [Error Handling](/development/webrtc/js-sdk/error-handling) — Error and warning codes - [Architecture](/development/webrtc/js-sdk/explanation/sdk-architecture) — How signaling and media flows work --- ### How WebRTC Signaling Works > Source: https://developers.telnyx.com/development/webrtc/js-sdk/explanation/webrtc-signaling.md # How WebRTC Signaling Works WebRTC itself has no signaling protocol — it only defines how to establish media. The signaling (how you say "call this number" or "I'm ringing") is up to the application. Here's how the Telnyx WebRTC SDK does it. --- ## The Signaling Path ```mermaid flowchart LR A[Your App] --> B[TelnyxRTC SDK] B --> C[WebSocket] C --> D[VSP] D --> E[SIP Proxy] E --> F[Carrier] G[B2BUA-RTC] -.-> D G -.->|Media| Client style C fill:#e1f5fe style D fill:#f3e5f5 style G fill:#fff3e0 ``` **Key components:** | Component | Role | Protocol | |-----------|------|----------| | **Your App** | User interface | JavaScript | | **TelnyxRTC SDK** | SDK logic | Internal | | **VSP** | Voice SDK Proxy — translates WebSocket ↔ SIP | WebSocket + SIP | | **SIP Proxy** | Routes SIP messages to carriers | SIP/UDP | | **B2BUA-RTC** | Media gateway (WebRTC ↔ RTP) | DTLS/SRTP + RTP | **VSP handles signaling only.** B2BUA-RTC handles media only. They are separate systems. --- ## WebSocket Connection The SDK opens a single persistent WebSocket to `rtc.telnyx.com`: ```javascript const client = new TelnyxRTC({ login_token: jwt }); client.connect(); // WebSocket lifecycle: // 1. DNS resolves rtc.telnyx.com → nearest VSP instance // 2. TLS handshake (port 443) // 3. WebSocket upgrade // 4. Login message sent (JWT or credential) // 5. Server responds with session info // 6. telnyx.ready fires — you can make/receive calls ``` ### What the DNS resolution does `rtc.telnyx.com` resolves to the nearest VSP based on DNS-based geo-routing: | Region | VSP DC | |--------|--------| | North America | NJ1 | | Europe | AMS3, FR5 | | Asia-Pacific | CN1 | If the DNS routes to a suboptimal VSP (e.g., an Indian client hitting FR5 instead of CN1), call latency increases. See [Configure Network & Firewall](/development/webrtc/js-sdk/how-to/configure-network-firewall) for troubleshooting. --- ## Outbound Call Flow When you call `client.newCall()`: ```mermaid sequenceDiagram participant C as Client (SDK) participant V as VSP participant S as SIP Proxy participant R as Carrier C->>V: invite V->>S: SIP INVITE S->>R: INVITE R-->>S: 100 Trying S-->>V: 180 Ringing V-->>C: callUpdate (state: ringing) R-->>S: 200 OK S-->>V: 200 OK V-->>C: callUpdate (state: active) Note over C,R: Media flows (RTP/SRTP) ``` **What the SDK does at each step:** 1. **`newCall()`** — Creates a Call object, starts ICE gathering 2. **SIP INVITE** — SDK sends invite message over WebSocket, VSP translates to SIP 3. **SDP negotiation** — Codec selection (OPUS, PCMU, PCMA), ICE candidates exchanged 4. **Ringing** — Remote party's phone is ringing. `call.state === 'ringing'` 5. **Answer (200 OK)** — Remote party picked up. `call.state === 'active'` 6. **Media flows** — Audio transmitted via WebRTC (separate from signaling) --- ## Inbound Call Flow When someone calls your WebRTC client: ```mermaid sequenceDiagram participant R as Carrier participant S as SIP Proxy participant V as VSP participant C as Client (SDK) R->>S: INVITE S->>V: SIP INVITE V->>C: invite C-->>V: (auto acknowledge) Note over C: callUpdate (state: ringing) C->>V: answer (call.answer()) V->>S: 200 OK S->>R: 200 OK Note over C,R: Media flows (RTP/SRTP) ``` **What the SDK does:** 1. **Incoming INVITE** — VSP receives SIP INVITE, pushes to SDK over WebSocket 2. **`callUpdate` notification** — `notification.call.state === 'ringing'` 3. **Your app decides** — Call `call.answer()` or `call.hangup()` 4. **Answer** — SDK sends 200 OK, establishes WebRTC media 5. **Media flows** — Two-way audio established --- ## Session Description Protocol (SDP) During call setup, both sides exchange SDP (Session Description Protocol) to agree on: | SDP Negotiates | Example | |----------------|---------| | **Audio codecs** | OPUS (preferred), PCMU (G.711u), PCMA (G.711a) | | **Codec parameters** | OPUS with FEC, stereo/mono, sample rate | | **ICE candidates** | How to reach each other for media | | **DTLS fingerprint** | For encrypting media (SRTP) | | **Media direction** | Sendrecv (two-way), sendonly, recvonly | | **Bandwidth** | Maximum bitrate | The SDK handles SDP negotiation automatically. You don't need to construct SDP manually. ### Codec Priority The SDK's default codec priority: 1. **OPUS** — Best quality, handles packet loss well, variable bitrate 2. **PCMU** — G.711μ-law, universal compatibility, 64kbps 3. **PCMA** — G.711A-law, European PSTN standard, 64kbps OPUS is strongly preferred — it handles jitter and packet loss better than G.711, and uses less bandwidth. --- ## WebSocket Reconnection If the WebSocket drops, the SDK automatically reconnects: ```mermaid flowchart LR A[Connected] -->|connection lost| B[Reconnecting 1s] B --> C[Reconnecting 2s] C --> D[...] D -->|success| A D -->|~30s exhausted| E[Disconnected] ``` See [Handle Reconnection](/development/webrtc/js-sdk/how-to/handle-reconnection) for the full reconnection behavior and how to handle it in your app. --- ## Custom Headers You can pass custom SIP headers in both directions: ### Outbound (your app → carrier) ```javascript const call = client.newCall({ destinationNumber: '+12345678900', customHeaders: [ { name: 'X-My-Header', value: 'value1' }, { name: 'X-Another', value: 'value2' }, ], }); ``` These appear as SIP headers in the INVITE. ### Inbound (carrier → your app) Inbound custom headers are available in the notification: ```javascript client.on('telnyx.notification', (notification) => { if (notification.type === 'callUpdate' && notification.call.state === 'ringing') { const customHeaders = notification.call.customHeaders; // e.g., { 'X-Caller-Name': 'John' } } }); ``` --- ## What Signals What | Action | SDK Method | SIP Message | Who Initiates | |--------|-----------|-------------|---------------| | Make a call | `newCall()` | INVITE | Client | | Answer a call | `call.answer()` | 200 OK | Client | | Reject a call | `call.hangup()` | 487 Request Terminated | Client | | End a call | `call.hangup()` | BYE | Either side | | Put on hold | `call.hold()` | re-INVITE with sendonly | Client | | Resume from hold | `call.unhold()` | re-INVITE with sendrecv | Client | | Send DTMF | `call.sendDigits()` | INFO (RFC 2833) | Client | | Mute audio | `call.mute()` | (local only, no SIP) | Client | Mute is a **local operation** — it stops sending audio from your microphone but doesn't send any SIP signal. The remote party doesn't know you're muted (unless you tell them via your app). --- ## See Also - [Call State Lifecycle](/development/webrtc/js-sdk/explanation/call-state-lifecycle) - [How ICE & TURN Work](/development/webrtc/js-sdk/explanation/ice-and-turn) - [Handle Reconnection](/development/webrtc/js-sdk/how-to/handle-reconnection) - [TelnyxRTC Class](/development/webrtc/js-sdk/reference/telnyxrtc) --- ### How ICE & TURN Work > Source: https://developers.telnyx.com/development/webrtc/js-sdk/explanation/ice-and-turn.md # How ICE & TURN Work If you've ever wondered why some calls sound great and others don't, the answer is often in ICE — the protocol that finds the best path for media between two endpoints. Understanding ICE helps you diagnose one-way audio, connection failures, and latency issues. --- ## The Problem ICE Solves Two devices want to send audio to each other. But between them: - **NAT (Network Address Translation)** — Private IPs (192.168.x.x) aren't reachable from the internet - **Firewalls** — Block incoming connections - **Symmetric NAT** — Even STUN can't discover the public mapping ICE (Interactive Connectivity Establishment) systematically tries every possible path and picks the best one. --- ## The Three Candidate Types ICE discovers three types of candidates, in order of preference: ### 1. Host Candidates (Local) Your device's local network interfaces (e.g., `192.168.1.105` on WiFi). - **Works when:** Both devices are on the same LAN (rare for WebRTC calls) - **Quality:** Best — zero extra latency - **Reality:** Almost never used for WebRTC calls — both parties are rarely on the same network ### 2. Server-Reflexive Candidates (srflx) — via STUN Your public IP, discovered by asking a STUN server (e.g., `203.0.113.5`). **How STUN works:** 1. SDK sends a request to `stun.telnyx.com:3478` 2. STUN server sees the source IP (your public IP) 3. STUN server sends it back: "Your public IP is 203.0.113.5" 4. SDK creates a srflx candidate with that IP - **Works when:** Your NAT allows inbound traffic to the mapped port (most home/office routers) - **Quality:** Good — direct path, minimal extra latency - **Blockers:** Symmetric NAT, strict firewalls ### 3. Relay Candidates — via TURN An IP address allocated on a TURN server that relays your media (e.g., `64.16.248.1`). **How TURN works:** 1. SDK authenticates with `turn.telnyx.com:3478` (UDP) or `turn.telnyx.com:443` (TCP) 2. TURN server allocates a relay address (e.g., `64.16.248.1:50000`) 3. All media is sent TO the TURN server, which forwards it to the remote party 4. Remote party also sends TO the TURN server, which forwards to you - **Works when:** Always — TURN is the fallback that never fails - **Quality:** Adds latency (each packet goes through the TURN server) but guarantees connectivity - **Required when:** Symmetric NAT, strict corporate firewalls, mobile carriers that block P2P --- ## ICE Candidate Priority The SDK tries candidates in this priority order: | Priority | Type | Path | Latency Impact | |----------|------|------|----------------| | 1 (best) | **Host** | Direct LAN | None | | 2 | **srflx** | Direct internet via STUN | Minimal | | 3 (worst) | **Relay** | Through TURN server | +20-80ms per direction | In practice, **most WebRTC calls use srflx (direct) or relay (TURN)**. Host candidates rarely work because both parties are on different networks. **A common misconception:** "If my call uses TURN relay, something is wrong." **False.** TURN relay is normal and expected in many network conditions — mobile networks, corporate networks, some ISPs. The question isn't "is TURN being used?" but "is the TURN server close to me?" --- ## ICE Gathering Process When a call starts, the SDK gathers candidates in this sequence: | Time | Event | Result | |------|-------|--------| | t=0ms | SDK starts ICE gathering | Host candidates collected (e.g., `192.168.1.105` WiFi, `10.0.0.2` Docker) | | ~50ms | STUN request to `stun.telnyx.com:3478` | srflx candidate discovered (e.g., `203.0.113.5:54321`) | | ~100ms | TURN allocate to `turn.telnyx.com:3478` | relay candidate allocated (e.g., `64.16.248.1:50000`) | | ~200ms | SDP sent to remote party with all candidates | (or sent incrementally with Trickle ICE) | ### Trickle ICE By default, the SDK uses **Trickle ICE** — it sends candidates as they're discovered rather than waiting for all of them: ```javascript const client = new TelnyxRTC({ login_token: jwt, trickleIce: true, // default in SDK 2.25.20+ }); ``` | Mode | Behavior | Call setup impact | |------|----------|-------------------| | **Without Trickle ICE** | SDK waits for ALL candidates (200-500ms) before sending INVITE | Slower setup | | **With Trickle ICE** | SDK sends INVITE immediately with host candidates, then sends srflx/relay as discovered | 200-500ms faster | --- ## ICE Connectivity Checks Once both sides have candidates, ICE performs connectivity checks in this order: | Step | Protocol | Purpose | |------|----------|---------| | 1 | **STUN Binding** | Can I reach you? Can you reach me? | | 2 | **Nomination** | This candidate pair works — let's use it | | 3 | **DTLS handshake** | Encrypt the media path | | 4 | **SRTP flows** | Encrypted audio starts | If the STUN check fails (e.g., firewall blocks it), ICE tries the next candidate pair until it finds one that works — falling back to TURN relay if necessary. --- ## DTLS — Encrypting Media After ICE finds a working path, DTLS (Datagram Transport Layer Security) encrypts the media: | Property | Value | |----------|-------| | Protocol | DTLS-SRTP (RFC 5764) | | Cipher | AES_CM_128_HMAC_SHA1_80 (most common) | | Key exchange | ECDHE (forward secrecy) | | Fingerprint | In SDP, verified during handshake | **DTLS states:** | State | Meaning | |-------|---------| | `new` | No handshake started | | `connecting` | Handshake in progress | | `connected` | Keys exchanged, media encrypted | | `failed` | Handshake failed (often a network/firewall issue) | If DTLS is stuck at `connecting`, media won't flow even if ICE connected. This is the #1 cause of one-way audio. --- ## TURN Server Selection Telnyx operates TURN servers in multiple regions: | Region | TURN Server | IP Range | |--------|-------------|----------| | US East | `turn-us-east.telnyx.com` | 64.16.248.x | | US West | `turn-us-west.telnyx.com` | 64.16.229.x | | Europe | `turn-eu.telnyx.com` | 185.183.x.x | | Asia-Pacific | `turn-apac.telnyx.com` | (varies) | The SDK automatically selects the nearest TURN server. You can override: ```javascript const client = new TelnyxRTC({ login_token: jwt, iceServers: [{ urls: 'turn:turn-eu.telnyx.com:443?transport=udp', username: '...', credential: '...', }], }); ``` ### UDP vs TCP TURN | Transport | Port | Latency | When to Use | |-----------|------|---------|-------------| | **UDP** (default) | 3478 | Lower | Most cases | | **TCP** | 443 | Higher (~20-40ms extra) | When UDP is blocked | | **TLS** | 443 | Highest | When TCP is blocked (rare) | The SDK tries UDP first, falls back to TCP automatically. --- ## Troubleshooting ICE Issues ### STUN fails (error 701) **Cause:** Firewall blocks `stun.telnyx.com:3478` (UDP) **Result:** No srflx candidates — must use relay **Fix:** Open UDP 3478 to STUN servers, or accept TURN relay ### All ICE fails **Cause:** Both STUN and TURN are blocked **Result:** Call cannot connect — no media path exists **Fix:** Open access to TURN servers (TCP 443 minimum) ### Relay when srflx should work **Cause:** Symmetric NAT — NAT mapping changes per destination **Result:** STUN-discovered port doesn't accept inbound from B2BUA-RTC **Fix:** This is normal; TURN relay is the correct solution ### High latency on relay **Cause:** TURN server is geographically distant **Result:** 100ms+ added round-trip **Fix:** Configure `iceServers` to use a closer TURN server --- ## See Also - [Configure Network & Firewall](/development/webrtc/js-sdk/how-to/configure-network-firewall) — Firewall rules and IP allowlists - [Debug Call Issues](/development/webrtc/js-sdk/how-to/debug-call-issues) — How to diagnose ICE/TURN problems - [Monitor Call Quality](/development/webrtc/js-sdk/how-to/monitor-call-quality) — Check ICE stats in production - [Call State Lifecycle](/development/webrtc/js-sdk/explanation/call-state-lifecycle) - [How WebRTC Signaling Works](/development/webrtc/js-sdk/explanation/webrtc-signaling) --- ### Authentication Architecture > Source: https://developers.telnyx.com/development/webrtc/js-sdk/explanation/authentication-architecture.md # Authentication Architecture Telnyx WebRTC has three authentication methods. They're not interchangeable — they form a hierarchy, and using the wrong one is the most common cause of security issues and unexpected behavior. --- ## The Hierarchy ```mermaid flowchart TD subgraph CC["Credential Connection"] direction TB subgraph TC["Telephony Credential"] direction TB JWT["JWT (Access Token)"] end end CC ---|has many| TC TC ---|generates many| JWT style CC fill:#e3f2fd,stroke:#1565c0 style TC fill:#f3e5f5,stroke:#7b1fa2 style JWT fill:#e8f5e9,stroke:#2e7d32 ``` - **One Credential Connection** can have **multiple Telephony Credentials** - **Each Telephony Credential** can generate **multiple JWTs** --- ## The Three Methods ### 1. Credential Connection (`login` + `password`) ```javascript const client = new TelnyxRTC({ login: 'my_credential', password: 'my_password', }); ``` **What it is:** Direct username/password authentication to the SIP infrastructure. **When to use:** Local development and testing only. **Limitations:** - Credentials are long-lived — they remain valid until manually deleted - No per-user isolation — one credential = one SIP registration - No automatic rotation or refresh **Portal page:** [Credential Connections](/development/webrtc/js-sdk/how-to/authenticating-your-app) --- ### 2. Telephony Credential (credential-based login) ```javascript const client = new TelnyxRTC({ login: 'credential_username', password: 'credential_password', // Same API, but using a Telephony Credential instead of Connection credential }); ``` **What it is:** A SIP identity (username + password) managed under a Credential Connection. **When to use:** Quick prototyping, testing with multiple identities. **One credential per user.** Never share a Telephony Credential across multiple users or browser tabs. Each credential = one SIP registration. If two tabs register with the same credential, only the most recent one receives incoming calls. **Portal page:** [Telephony Credentials](/development/webrtc/js-sdk/how-to/authenticating-your-app) --- ### 3. JWT (`login_token`) ```javascript const client = new TelnyxRTC({ login_token: 'eyJhbGciOiJSUzI1NiIs...', // JWT string }); ``` **What it is:** A time-limited, cryptographically signed token that authenticates your client. **When to use:** Production. Always. This is the recommended method. **Why it's recommended:** - **Time-limited** — tokens expire 24 hours after creation (or when the parent credential expires, whichever comes first) - **Per-device** — each device should use its own credential → its own JWT, preventing registration conflicts - **Refresh-aware** — the SDK emits a `TOKEN_EXPIRING_SOON` warning (code 34001) before expiry, so your app can request a fresh token from your backend - **Multiple concurrent sessions** — different users/devices can each have their own JWT without overwriting each other's SIP registration **Portal page:** [JWT Authentication](/development/webrtc/js-sdk/how-to/authenticating-your-app) --- ## Comparison | | Credential Connection | Telephony Credential | JWT | |---|---|---|---| | **SDK field** | `login` + `password` | `login` + `password` | `login_token` | | **Anonymous** | — | — | `anonymous_login` (object) | | **Lifetime** | Permanent | Permanent | 24 hours (or credential expiry) | | **In browser?** | Dev only | Dev only | Production-ready | | **Per-user** | No | Yes | Yes | | **Revocable** | Delete credential | Delete credential | Disable parent credential | | **Rotation** | Manual | Manual | App handles via TOKEN_EXPIRING_SOON | | **Incoming calls** | Single registration | Single registration | Multi-user | | **Setup effort** | Low | Low | Medium (needs backend) | --- ## The JWT Flow in Production ### Initial connection ```mermaid sequenceDiagram participant B as Browser participant A as Your Backend participant T as Telnyx API participant V as VSP B->>A: 1. Request token A->>T: 2. POST /telnyx_rtc/access_tokens T-->>A: { token: "eyJ..." } A-->>B: Return login_token B->>V: 3. Connect with JWT V-->>B: telnyx.ready ✓ ``` ### Token refresh JWTs expire after 24 hours. The SDK warns you before expiry so you can refresh without disconnecting: ```mermaid sequenceDiagram participant B as Browser participant A as Your Backend participant T as Telnyx API Note over B: TOKEN_EXPIRING_SOON (34001) B->>A: 4. Request new token A->>T: 5. POST /telnyx_rtc/access_tokens T-->>A: { token: "new_eyJ..." } A-->>B: Return new login_token Note over B: client.updateToken(newToken) Note over B: Session continues ✓ ``` **Your backend must:** 1. Have a Telnyx API key (`TELNYX_API_KEY`) 2. Expose an endpoint that creates tokens for a given telephony credential 3. Return the token to the browser 4. Handle token refresh requests when `TOKEN_EXPIRING_SOON` (34001) fires See [Authenticating Your App](/development/webrtc/js-sdk/how-to/authenticating-your-app) for the full implementation. --- ## Why "One Credential Per User" Matters ### Wrong — shared credential ```mermaid flowchart LR A[User A] -->|register shared credential| V[VSP] B[User B] -->|register shared credential| V V -->|overwrites A's registration| X[Only B rings] A -.- X style A fill:#ffcdd2 style B fill:#c8e6c9 style X fill:#ffcdd2 ``` When two users share one credential, the second registration overwrites the first. **User A never receives incoming calls.** ### Correct — separate JWTs ```mermaid flowchart LR A[User A] -->|register JWT_A| V[VSP] B[User B] -->|register JWT_B| V V -->|A's registration| A2[A rings] V -->|B's registration| B2[B rings] style A fill:#c8e6c9 style B fill:#c8e6c9 style A2 fill:#c8e6c9 style B2 fill:#c8e6c9 ``` With JWTs, each token maps to a unique session. Multiple users can register concurrently without conflicts. --- ## Common Mistakes | Mistake | What Happens | Fix | |---------|-------------|-----| | Using `login`+`password` for production | Long-lived credential, no rotation, no per-device isolation | Use `login_token` (JWT) | | Sharing one credential across users | Only last user gets incoming calls | One credential/JWT per user | | Not handling `TOKEN_EXPIRING_SOON` (34001) | Token expires → disconnected after 24h | Implement token refresh in your app | | Generating JWT in the browser | API key in client code | Generate JWT on your backend only | --- ## See Also - [Authenticating Your App](/development/webrtc/js-sdk/how-to/authenticating-your-app) — Full code examples for all three methods - [IClientOptions](/development/webrtc/js-sdk/reference/iclientoptions) — `login_token`, `login`, `password` fields --- ### Call State Lifecycle > Source: https://developers.telnyx.com/development/webrtc/js-sdk/explanation/call-state-lifecycle.md # Call State Lifecycle A call is a state machine. Understanding every state and transition is essential for building a reliable UI and handling edge cases like reconnection, transfer, and one-way audio. --- ## State Diagram ```mermaid stateDiagram-v2 [*] --> new: newCall() new --> ringing: INVITE sent/received ringing --> active: answer() / 200 OK ringing --> destroyed: hangup() / reject active --> held: hold() held --> active: unhold() active --> destroyed: hangup() / BYE received held --> destroyed: hangup() active --> reconnecting: ICE/DTLS failure reconnecting --> active: media restored reconnecting --> destroyed: timeout ``` --- ## All States | State | Direction | Description | What your app should do | |-------|-----------|-------------|------------------------| | `new` | Outbound | Call object created, ICE gathering | Show "Connecting..." | | `ringing` | Outbound | Remote party's phone is ringing | Show ringing UI, play ringback tone | | `ringing` | Inbound | Incoming call waiting | Show incoming call UI, ring tone | | `active` | Both | Call connected, media flowing | Show in-call UI, start timer | | `held` | Both | Call on hold (sendonly) | Show held state, dim audio | | `reconnecting` | Both | Media path lost, attempting recovery | Show reconnecting banner | | `destroyed` | Both | Call ended | Show call ended, clean up UI | --- ## Outbound Call States (Detailed) ### `new` → `ringing` ```javascript const call = client.newCall({ destinationNumber: '+12345678900', audio: true, }); // call.state === 'new' // SDK is: gathering ICE candidates, preparing SDP, sending INVITE ``` **What happens internally:** 1. SDK creates a PeerConnection 2. ICE gathering starts (host → srflx → relay candidates) 3. SDP offer created with codec preferences 4. INVITE sent over WebSocket to VSP 5. VSP translates to SIP INVITE → carrier **Your app:** Show "Calling..." with a spinner. Don't start the call timer yet. ### `ringing` ```javascript // Remote party's phone is ringing // call.state === 'ringing' ``` **What happens:** - Remote phone is ringing (SIP 180 Ringing) - You may hear ringback tone (generated locally by the SDK or played from network) **Your app:** Play ringback tone if SDK doesn't auto-play it. Show "Ringing..." state. ### `ringing` → `active` ```javascript // Remote party answered // call.state === 'active' ``` **What happens internally:** 1. SIP 200 OK received from carrier 2. SDP answer processed — codecs and ICE candidates agreed 3. DTLS handshake completes — media is encrypted 4. SRTP audio starts flowing in both directions 5. Audio element auto-created and attached to DOM **Your app:** Start call timer. Show in-call controls (mute, hold, hangup). Check audio is playing. --- ## Inbound Call States (Detailed) ### `ringing` (incoming) ```javascript client.on('telnyx.notification', (notification) => { if (notification.type === 'callUpdate') { const call = notification.call; if (call.state === 'ringing' && call.direction === 'inbound') { // Incoming call! const from = call.remotePartyNumber; const to = call.remotePartyName; showIncomingCallUI({ from, to, call }); } } }); ``` **What happens internally:** 1. VSP receives SIP INVITE from carrier 2. VSP pushes invite message to SDK over WebSocket 3. SDK creates a Call object with `state: 'ringing'` 4. `telnyx.notification` fires with `callUpdate` **Your app:** Show incoming call UI. Play ringtone. Offer Accept/Reject buttons. ### `ringing` → `active` (answer) ```javascript // User clicks "Accept" call.answer(); // call.state → 'active' ``` **What happens internally:** 1. SDK sends 200 OK over WebSocket 2. getUserMedia() — browser requests microphone permission 3. ICE gathering starts 4. SDP answer sent 5. DTLS handshake 6. Media flows ** Important:** `call.answer()` triggers `getUserMedia()`. If the user hasn't granted microphone permission, the browser will show a permission dialog. The call won't be fully active until permission is granted. ### `ringing` → `destroyed` (reject) ```javascript // User clicks "Reject" call.hangup(); // call.state → 'destroyed' ``` **What happens:** SDK sends SIP 487 Request Terminated (or CANCEL if INVITE still in progress). --- ## Active Call States ### `active` → `held` ```javascript call.hold(); // call.state → 'held' ``` **What happens:** 1. SDK sends re-INVITE with `sendonly` media direction 2. Remote party's audio continues (they hear hold music if configured) 3. Your audio stops sending (microphone muted at SIP level) 4. Remote party receives a `callUpdate` with their call state changing ### `held` → `active` ```javascript call.unhold(); // call.state → 'active' ``` **What happens:** 1. SDK sends re-INVITE with `sendrecv` media direction 2. Two-way audio resumes --- ## Reconnecting State ```javascript // Network interruption during call // call.state → 'reconnecting' ``` **What triggers it:** - ICE connectivity checks fail (network change) - DTLS session breaks - WebSocket still connected but media path lost **What the SDK does:** 1. ICE restart — re-gathers candidates 2. Attempts to re-establish DTLS 3. If successful → `call.state → 'active'` (call resumes) 4. If fails after timeout → `call.state → 'destroyed'` (call drops) **Your app:** Show a "Reconnecting..." banner. Don't hang up — let the SDK try to recover. See [Handle Reconnection](/development/webrtc/js-sdk/how-to/handle-reconnection) for details. --- ## Destroyed State All calls end up here. It's terminal — no further transitions. ```javascript client.on('telnyx.notification', (notification) => { if (notification.call.state === 'destroyed') { const call = notification.call; console.log(`Call ended. Direction: ${call.direction}`); console.log(`Duration: ${call.duration}s`); console.log(`End reason: ${call.cause}`); // normal, hangup, timeout, error } }); ``` **Common causes:** | Cause | Direction | Why | |-------|-----------|-----| | `normal` | Both | Normal hangup — either party ended the call | | `originatorCancel` | Outbound | Caller hung up while ringing | | `timeOut` | Outbound | No answer within timeout period | | `rejected` | Inbound | Callee rejected the call | | `error` | Both | Network error, ICE failure, or server error | | `replaced` | Both | Call was replaced (attended transfer) | **Your app:** Clean up UI. Upload call report. Show call summary if applicable. --- ## State Transition Matrix | From | To | Trigger | Direction | |------|----|---------|-----------| | `new` | `ringing` | INVITE sent/received | Outbound | | `ringing` | `active` | `answer()` / 200 OK | Both | | `ringing` | `destroyed` | `hangup()` / reject / timeout | Both | | `active` | `held` | `hold()` | Both | | `held` | `active` | `unhold()` | Both | | `active` | `destroyed` | `hangup()` / BYE | Both | | `held` | `destroyed` | `hangup()` | Both | | `active` | `reconnecting` | ICE/DTLS failure | Both | | `reconnecting` | `active` | Media restored | Both | | `reconnecting` | `destroyed` | Timeout | Both | --- ## Common Pitfalls ### Double answer ```javascript // WRONG — answering an already-active call call.answer(); // First answer — starts PeerConnection call.answer(); // Second answer — creates ANOTHER PeerConnection! // Result: Two PeerConnections, the second one never connects properly // SDK bug: CallReportCollector may track the wrong PC → reports show zero audio ``` **Fix:** Guard against double answer in your UI: ```javascript let answered = false; function answerCall(call) { if (answered || call.state !== 'ringing') return; answered = true; call.answer(); } ``` ### Not handling `destroyed` ```javascript // WRONG — only checking for 'active' if (call.state === 'active') { startTimer(); } // What if the call goes to 'destroyed'? Timer keeps running forever. // CORRECT — handle both if (call.state === 'active') { startTimer(); } else if (call.state === 'destroyed') { stopTimer(); cleanupCall(call); } ``` ### Missing `reconnecting` ```javascript // If you don't handle reconnecting, the user thinks the call dropped // and tries to call again — creating a second call // Show a banner so the user knows to wait if (call.state === 'reconnecting') { showReconnectingBanner(); } ``` --- ## See Also - [Call Class](/development/webrtc/js-sdk/reference/call) — Methods for each state - [INotification](/development/webrtc/js-sdk/reference/inotification) — All notification types - [Handle Reconnection](/development/webrtc/js-sdk/how-to/handle-reconnection) — Reconnection flow - [Handle Multiple Calls](/development/webrtc/js-sdk/explanation/call-state-lifecycle) — Hold/resume patterns - [How WebRTC Signaling Works](/development/webrtc/js-sdk/explanation/webrtc-signaling) — SIP message flow --- ### WebRTC JS ChangeLog > Source: https://developers.telnyx.com/development/webrtc/js-sdk/changelog.md --- ### React quickstart > Source: https://developers.telnyx.com/development/webrtc/react-sdk.md The React SDK can be added to your application by installing the npm packages: ```bash npm install --save @telnyx/react-client @telnyx/webrtc ``` ## Client initialization In the `TelnyxRTCProvider` component, you can pass credentials and options objects with custom ringtones: ```jsx // App.jsx import { TelnyxRTCProvider } from '@telnyx/react-client'; function App() { const credential = { login_token: 'mytoken', }; const options = { ringtoneFile: "./ringtone.mp3", ringbackFile: "./ringback.mp3", }; return ( ); } ``` ## Phone component In the `Phone` component, you would subscribe to the notifications from the WebRTC client, specify callbacks for Telnyx client event handlers, and define an Audio element. First import the React client: ```jsx import { useNotification, Audio, useCallbacks } from "@telnyx/react-client"; ``` Define a `Phone` function component where you will manage event handlers using callbacks and control audio stream in the `` element: ```jsx const Phone = () => { const notification = useNotification(); const activeCall = notification && notification.call; useCallbacks({ onReady: (status) => { console.log("WebRTC client ready"); console.log(status); }, onError: (error) => { console.log("WebRTC client error"); console.error(error); }, onSocketError: (error) => { console.log("WebRTC client socket error"); console.error(error); }, onSocketClose: () => { console.log("WebRTC client socket closed"); }, onNotification: (message) => { console.log("WebRTC client notification:", message); if (message.type === "callUpdate") { const call = message.call; console.log("Call state:", call.state); } }, }); return ( ); }; ``` ## Sample React app Check out our [sample React application](https://github.com/team-telnyx/webrtc-examples/tree/main/react-client/react-app) for a full implementation of the Telnyx Voice SDK with React components. --- ### Voice native iOS Client SDK > Source: https://developers.telnyx.com/development/webrtc/ios-sdk.md --- ### WebRTC iOS Client > Source: https://developers.telnyx.com/development/webrtc/ios-sdk/classes/txclient.md --- ### WebRTC iOS Call > Source: https://developers.telnyx.com/development/webrtc/ios-sdk/classes/call.md --- ### WebRTC iOS Call > Source: https://developers.telnyx.com/development/webrtc/ios-sdk/classes/call-extensions.md --- ### WebRTC iOS Client > Source: https://developers.telnyx.com/development/webrtc/ios-sdk/classes/txclient-extensions.md --- ### WebRTC iOS Call State > Source: https://developers.telnyx.com/development/webrtc/ios-sdk/enums/call-state.md --- ### WebRTC iOS TxClientDelegate > Source: https://developers.telnyx.com/development/webrtc/ios-sdk/protocols/tx-client-delegate.md --- ### WebRTC iOS Call Info > Source: https://developers.telnyx.com/development/webrtc/ios-sdk/structs/tx-call-info.md --- ### WebRTC iOS Client Configuration > Source: https://developers.telnyx.com/development/webrtc/ios-sdk/structs/tx-config.md --- ### WebRTC iOS Client Push Notifications Configuration > Source: https://developers.telnyx.com/development/webrtc/ios-sdk/structs/tx-push-config.md --- ### WebRTC iOS Client Push IP Notifications Configuration > Source: https://developers.telnyx.com/development/webrtc/ios-sdk/structs/tx-push-ip-config.md --- ### iOS Portal Setup > Source: https://developers.telnyx.com/development/webrtc/ios-sdk/push-notification/portal-setup.md --- ### iOS Push Notification Setup > Source: https://developers.telnyx.com/development/webrtc/ios-sdk/push-notification/app-setup.md --- ### Troubleshooting > Source: https://developers.telnyx.com/development/webrtc/ios-sdk/push-notification/troubleshooting.md --- ### WebRTC iOS SDK AI Voice Assistant Introduction > Source: https://developers.telnyx.com/development/webrtc/ios-sdk/ai-voice-assistant/introduction.md --- ### WebRTC iOS SDK AI Voice Assistant Anonymous Login > Source: https://developers.telnyx.com/development/webrtc/ios-sdk/ai-voice-assistant/anonymous-login.md --- ### WebRTC iOS SDK AI Voice Assistant Starting Conversations > Source: https://developers.telnyx.com/development/webrtc/ios-sdk/ai-voice-assistant/starting-conversations.md --- ### WebRTC iOS SDK AI Voice Assistant Text Messaging > Source: https://developers.telnyx.com/development/webrtc/ios-sdk/ai-voice-assistant/text-messaging.md --- ### WebRTC iOS SDK AI Voice Assistant Transcript Updates > Source: https://developers.telnyx.com/development/webrtc/ios-sdk/ai-voice-assistant/transcript-updates.md --- ### WebRTC Stats > Source: https://developers.telnyx.com/development/webrtc/ios-sdk/stats.md --- ### WebRTC iOS SDK Error Handling > Source: https://developers.telnyx.com/development/webrtc/ios-sdk/error-handling.md --- ### WebRTC iOS ChangeLog > Source: https://developers.telnyx.com/development/webrtc/ios-sdk/changelog.md --- ### WebRTC Android Quickstart > Source: https://developers.telnyx.com/development/webrtc/android-sdk/quickstart.md --- ### Android Voice Client SDK > Source: https://developers.telnyx.com/development/webrtc/android-sdk.md --- ### WebRTC Android Client > Source: https://developers.telnyx.com/development/webrtc/android-sdk/classes/txclient.md --- ### WebRTC Android Call > Source: https://developers.telnyx.com/development/webrtc/android-sdk/classes/call.md --- ### WebRTC Android Config > Source: https://developers.telnyx.com/development/webrtc/android-sdk/config/txconfig.md --- ### WebRTC Android ReceivedMessageBody > Source: https://developers.telnyx.com/development/webrtc/android-sdk/socket/receivedmessagebody.md --- ### WebRTC Android SocketResponse > Source: https://developers.telnyx.com/development/webrtc/android-sdk/socket/socketresponse.md --- ### Android Portal Setup > Source: https://developers.telnyx.com/development/webrtc/android-sdk/push-notification/portal-setup.md --- ### Notification Quickstart for Android > Source: https://developers.telnyx.com/development/webrtc/android-sdk/push-notification/quickstart.md --- ### Android Push Notification Setup > Source: https://developers.telnyx.com/development/webrtc/android-sdk/push-notification/app-setup.md --- ### Android Push Troubleshooting > Source: https://developers.telnyx.com/development/webrtc/android-sdk/push-notification/troubleshooting.md --- ### WebRTC Android SDK AI Voice Assistant Introduction > Source: https://developers.telnyx.com/development/webrtc/android-sdk/ai-voice-assistant/introduction.md --- ### WebRTC Android SDK AI Voice Assistant Anonymous Login > Source: https://developers.telnyx.com/development/webrtc/android-sdk/ai-voice-assistant/anonymous-login.md --- ### WebRTC Android SDK AI Voice Assistant Starting Conversations > Source: https://developers.telnyx.com/development/webrtc/android-sdk/ai-voice-assistant/starting-conversations.md --- ### WebRTC Android SDK AI Voice Assistant Text Messaging > Source: https://developers.telnyx.com/development/webrtc/android-sdk/ai-voice-assistant/text-messaging.md --- ### WebRTC Android SDK AI Voice Assistant Transcript Updates > Source: https://developers.telnyx.com/development/webrtc/android-sdk/ai-voice-assistant/transcript-updates.md --- ### WebRTC Stats > Source: https://developers.telnyx.com/development/webrtc/android-sdk/stats.md --- ### WebRTC Call Reports > Source: https://developers.telnyx.com/development/webrtc/android-sdk/call-reports.md # WebRTC Call Reports The Telnyx Android SDK automatically collects detailed call statistics during WebRTC calls and sends them to Telnyx for troubleshooting and monitoring purposes. This feature helps diagnose call quality issues, connection problems, and provides insights into call performance. ## Overview When enabled, the SDK collects: - **Call summary**: Call identifiers, timestamps, duration, and device information - **Connection metrics**: ICE states, DTLS states, signaling transitions - **Media statistics**: Packet loss, jitter, round-trip time, audio levels - **Network information**: Selected ICE candidates, transport details The data is automatically uploaded to Telnyx when a call ends, and can also be accessed locally for debugging. ## Enabling Call Reports Call reports are automatically enabled when you connect to the Telnyx WebRTC service. The SDK handles data collection and upload transparently. Call reports are sent automatically when a call ends. No additional configuration is required. ## Accessing Local Call Reports For debugging purposes, you can access the call report JSON file that is saved locally after each call: ```kotlin // Get the path to the last generated call stats JSON file val jsonPath = telnyxClient.getLastCallStatsJsonPath() // Read and process the JSON file jsonPath?.let { path -> val file = File(path) if (file.exists()) { val jsonContent = file.readText() // Process the call stats JSON } } ``` Call stats files are saved to: `/call_stats/call_stats_.json` ## Call Report JSON Structure The call report JSON contains the following sections: ### Top-Level Identifiers ```json { "call_id": "uuid-of-the-call", "call_report_id": "report-token-from-server", "telnyx_leg_id": "uuid-of-telnyx-leg", "telnyx_session_id": "uuid-of-telnyx-session", "voice_sdk_id": "websocket-session-id" } ``` ### Summary Section Contains high-level call information: ```json { "summary": { "callId": "uuid-of-the-call", "callerNumber": "+1234567890", "destinationNumber": "+0987654321", "direction": "outbound", "durationSeconds": 120.5, "startTimestamp": "2026-03-23T12:00:00.000Z", "endTimestamp": "2026-03-23T12:02:00.500Z", "sdkVersion": "3.5.0", "state": "done", "telnyxLegId": "uuid", "telnyxSessionId": "uuid" } } ``` ### Stats Array Contains interval-based statistics captured during the call. Each interval (typically 5 seconds) includes: ```json { "stats": [ { "audio": { "inbound": { "bytesReceived": 48000, "packetsReceived": 500, "packetsLost": 2, "packetsDiscarded": 0, "jitterAvg": 10.5, "jitterBufferDelay": 50.0, "concealedSamples": 100, "concealmentEvents": 1, "audioLevelAvg": 0.85 }, "outbound": { "bytesSent": 48000, "packetsSent": 500, "audioLevelAvg": 0.78 } }, "connection": { "bytesReceived": 50000, "bytesSent": 50000, "packetsReceived": 520, "packetsSent": 520, "roundTripTimeAvg": 45.2 }, "ice": { "local": { "candidateType": "host", "protocol": "udp", "address": "192.168.1.100", "port": 54321 }, "remote": { "candidateType": "srflx", "protocol": "udp", "address": "203.0.113.50", "port": 12345 }, "nominated": true, "state": "succeeded" }, "transport": { "dtlsState": "connected", "iceState": "connected", "srtpCipher": "AEAD_AES_128_GCM", "tlsVersion": "DTLS 1.2" }, "intervalStartUtc": "2026-03-23T12:00:00.000Z", "intervalEndUtc": "2026-03-23T12:00:05.000Z" } ] } ``` ### Android Extra Section Contains Android-specific debugging information: ```json { "android_extra": { "deviceInfo": { "userAgent": "TelnyxAndroidSDK/3.5.0", "networkType": "WiFi", "osVersion": "Android 14 (API 34)", "deviceModel": "Google Pixel 8", "selectedCodec": "audio/opus", "permissions": { "microphone": true, "notifications": true } }, "connectionState": { "lastIceGatheringState": "complete", "lastIceConnectionState": "connected", "lastDtlsState": "connected", "lastSignalingState": "stable" }, "connectionTimeline": [ { "event": "ICE_CHECKING", "timestamp": "2026-03-23T12:00:00.100Z" }, { "event": "ICE_CONNECTED", "timestamp": "2026-03-23T12:00:00.500Z" }, { "event": "DTLS_CONNECTING", "timestamp": "2026-03-23T12:00:00.600Z" }, { "event": "DTLS_CONNECTED", "timestamp": "2026-03-23T12:00:01.200Z" } ], "iceCandidates": { "total": 6, "hostCount": 2, "srflxCount": 2, "relayCount": 2 } } } ``` ## Metrics Reference ### Audio Inbound Metrics | Metric | Description | |--------|-------------| | `bytesReceived` | Total bytes received | | `packetsReceived` | Total packets received | | `packetsLost` | Number of lost packets | | `packetsDiscarded` | Packets discarded by jitter buffer | | `jitterAvg` | Average jitter in milliseconds | | `jitterBufferDelay` | Jitter buffer delay in milliseconds | | `concealedSamples` | Number of concealed audio samples | | `concealmentEvents` | Number of concealment events | | `audioLevelAvg` | Average audio level (0.0 to 1.0) | ### Audio Outbound Metrics | Metric | Description | |--------|-------------| | `bytesSent` | Total bytes sent | | `packetsSent` | Total packets sent | | `audioLevelAvg` | Average outgoing audio level (0.0 to 1.0) | ### Connection Metrics | Metric | Description | |--------|-------------| | `roundTripTimeAvg` | Average round-trip time in milliseconds | | `bytesReceived` | Total transport bytes received | | `bytesSent` | Total transport bytes sent | ## Troubleshooting with Call Reports Call reports can help diagnose common issues: ### High Packet Loss Look at `packetsLost` in the audio inbound stats. Values above 1-2% may indicate network issues. ### Audio Quality Issues Check `jitterAvg` and `concealmentEvents`. High jitter (>30ms) or frequent concealment events indicate audio quality degradation. ### Connection Failures Review the `connectionTimeline` in `android_extra`. Look for: - `ICE_FAILED` - Network connectivity issues - `DTLS_FAILED` - TLS/security handshake failures - Long gaps between `ICE_CONNECTED` and `DTLS_CONNECTED` (>5 seconds) ### One-Way Audio Check if: - `packetsReceived` is 0 (not receiving audio) - `audioLevelAvg` is 0 (microphone not capturing) - Review microphone permissions in `deviceInfo.permissions` ## Privacy & Data Handling Call reports are sent securely to Telnyx and are used solely for: - Troubleshooting call quality issues - Monitoring service health - Improving SDK performance Call reports do not include actual audio content or personally identifiable information beyond call metadata. ## See Also - [WebRTC Stats](/development/webrtc/android-sdk/stats) - Real-time call quality metrics - [Error Handling](/development/webrtc/android-sdk/error-handling) - SDK error codes and handling - [Troubleshooting Guide](/docs/voice/webrtc/push-notifications/android/troubleshooting) - Common issues and solutions --- ### WebRTC Android SDK Error Handling > Source: https://developers.telnyx.com/development/webrtc/android-sdk/error-handling.md --- ### WebRTC Android ChangeLog > Source: https://developers.telnyx.com/development/webrtc/android-sdk/changelog.md --- ### React Native SDK > Source: https://developers.telnyx.com/development/webrtc/react-native-sdk.md --- ### React Native SDK Quickstart > Source: https://developers.telnyx.com/development/webrtc/react-native-sdk/quickstart.md --- ### Call > Source: https://developers.telnyx.com/development/webrtc/react-native-sdk/classes/call.md --- ### TelnyxVoipClient > Source: https://developers.telnyx.com/development/webrtc/react-native-sdk/classes/telnyxvoipclient.md --- ### CredentialConfig > Source: https://developers.telnyx.com/development/webrtc/react-native-sdk/interfaces/credentialconfig.md --- ### TokenConfig > Source: https://developers.telnyx.com/development/webrtc/react-native-sdk/interfaces/tokenconfig.md --- ### TelnyxVoipClientOptions > Source: https://developers.telnyx.com/development/webrtc/react-native-sdk/interfaces/telnyxvoipclientoptions.md --- ### TelnyxVoiceAppOptions > Source: https://developers.telnyx.com/development/webrtc/react-native-sdk/interfaces/telnyxvoiceappoptions.md --- ### Portal Setup > Source: https://developers.telnyx.com/development/webrtc/react-native-sdk/push-notification/portal-setup.md --- ### App Setup > Source: https://developers.telnyx.com/development/webrtc/react-native-sdk/push-notification/app-setup.md --- ### Error Handling > Source: https://developers.telnyx.com/development/webrtc/react-native-sdk/error-handling.md --- ### Changelog > Source: https://developers.telnyx.com/development/webrtc/react-native-sdk/changelog.md --- ### Flutter Voice Client SDK > Source: https://developers.telnyx.com/development/webrtc/flutter-sdk.md --- ### WebRTC Flutter Client > Source: https://developers.telnyx.com/development/webrtc/flutter-sdk/classes/txclient.md --- ### WebRTC Flutter Call > Source: https://developers.telnyx.com/development/webrtc/flutter-sdk/classes/call.md --- ### Flutter WebRTC SDK Message > Source: https://developers.telnyx.com/development/webrtc/flutter-sdk/classes/messages/telnyx-message.md --- ### Flutter WebRTC SDK Socket Error Message > Source: https://developers.telnyx.com/development/webrtc/flutter-sdk/classes/messages/telnyx-socket-error.md --- ### WebRTC Flutter Call State > Source: https://developers.telnyx.com/development/webrtc/flutter-sdk/enums/call-state.md --- ### Flutter WebRTC SDK Socket Message Handler > Source: https://developers.telnyx.com/development/webrtc/flutter-sdk/event-handlers/on-socket-message-received.md --- ### Flutter WebRTC SDK Socket Error Handler > Source: https://developers.telnyx.com/development/webrtc/flutter-sdk/event-handlers/on-socket-error-received.md --- ### WebRTC Flutter Client Configuration > Source: https://developers.telnyx.com/development/webrtc/flutter-sdk/method-objects/config.md --- ### WebRTC Flutter Incoming Invite Object > Source: https://developers.telnyx.com/development/webrtc/flutter-sdk/method-objects/incoming-invite-params.md --- ### WebRTC Flutter Client Push Notifications Configuration > Source: https://developers.telnyx.com/development/webrtc/flutter-sdk/method-objects/push-metadata.md --- ### Flutter Push Notification Portal Setup > Source: https://developers.telnyx.com/development/webrtc/flutter-sdk/push-notification/portal-setup.md --- ### Flutter Push Notification App Setup > Source: https://developers.telnyx.com/development/webrtc/flutter-sdk/push-notification/app-setup.md --- ### Flutter Push Troubleshooting > Source: https://developers.telnyx.com/development/webrtc/flutter-sdk/push-notification/troubleshooting.md --- ### WebRTC Flutter SDK AI Voice Assistant Introduction > Source: https://developers.telnyx.com/development/webrtc/flutter-sdk/ai-voice-assistant/introduction.md --- ### WebRTC Flutter SDK AI Voice Assistant Anonymous Login > Source: https://developers.telnyx.com/development/webrtc/flutter-sdk/ai-voice-assistant/anonymous-login.md --- ### WebRTC Flutter SDK AI Voice Assistant Starting Conversations > Source: https://developers.telnyx.com/development/webrtc/flutter-sdk/ai-voice-assistant/starting-conversations.md --- ### WebRTC Flutter SDK AI Voice Assistant Text Messaging > Source: https://developers.telnyx.com/development/webrtc/flutter-sdk/ai-voice-assistant/text-messaging.md --- ### WebRTC Flutter SDK AI Voice Assistant Transcript Updates > Source: https://developers.telnyx.com/development/webrtc/flutter-sdk/ai-voice-assistant/transcript-updates.md --- ### WebRTC Stats > Source: https://developers.telnyx.com/development/webrtc/flutter-sdk/stats.md --- ### WebRTC Flutter SDK Error Handling > Source: https://developers.telnyx.com/development/webrtc/flutter-sdk/error-handling.md --- ### WebRTC Flutter ChangeLog > Source: https://developers.telnyx.com/development/webrtc/flutter-sdk/changelog.md --- ## API Reference (WebRTC) ### Credentials - [List all credentials](https://developers.telnyx.com/api-reference/credentials/list-all-credentials.md): List all On-demand Credentials. - [Create a credential](https://developers.telnyx.com/api-reference/credentials/create-a-credential.md): Create a credential. - [Get a credential](https://developers.telnyx.com/api-reference/credentials/get-a-credential.md): Get the details of an existing On-demand Credential. - [Update a credential](https://developers.telnyx.com/api-reference/credentials/update-a-credential.md): Update an existing credential. - [Delete a credential](https://developers.telnyx.com/api-reference/credentials/delete-a-credential.md): Delete an existing credential. ### Access Tokens - [Create an Access Token.](https://developers.telnyx.com/api-reference/access-tokens/create-an-access-token.md): Create an Access Token (JWT) for the credential. ### Push Credentials - [List mobile push credentials](https://developers.telnyx.com/api-reference/push-credentials/list-mobile-push-credentials.md): List mobile push credentials - [Creates a new mobile push credential](https://developers.telnyx.com/api-reference/push-credentials/creates-a-new-mobile-push-credential.md): Creates a new mobile push credential - [Retrieves a mobile push credential](https://developers.telnyx.com/api-reference/push-credentials/retrieves-a-mobile-push-credential.md): Retrieves mobile push credential based on the given `push_credential_id` - [Deletes a mobile push credential](https://developers.telnyx.com/api-reference/push-credentials/deletes-a-mobile-push-credential.md): Deletes a mobile push credential based on the given `push_credential_id` ### Voice SDK Stats - [List Voice SDK call reports](https://developers.telnyx.com/api-reference/voice-sdk-stats/list-voice-sdk-call-reports.md): Returns paginated raw call report stats JSON payloads stored for the authenticated user. The user is derived from Telnyx authentication, not from request param… - [Retrieve Voice SDK call reports by call ID](https://developers.telnyx.com/api-reference/voice-sdk-stats/retrieve-voice-sdk-call-reports-by-call-id.md): Returns raw call report stats JSON payloads stored for the authenticated user and `call_id`. The user is derived from Telnyx authentication, not from request p…