---
title: Integration Guide
slug: voice-apis/integration-guide
docTags: 
createdAt: 2025-10-03T04:23:31.466Z
---

### Introduction

ExotelVoice library enables you to add the voip calling feature into your app. This document outlines the integration steps. The library supports only peer to peer 2-way calls. Multi-party conferencing use cases are not supported. 

*Section 4* describes integration steps for ExotelVoice IOS SDK. *Section 5.1* describes the webhooks that should be supported in your application backend for number-masking workflow.  *Section 5.2.1* describes subscribers  provisioning in the Exotel platform.

### Licensing

Create a trial *account* in Exotel. To enable voip calling in the trial account, contact **exotel support**. Once Exotel support enables the voip capability in the account , a VOIP Exophone will be created as shown below and available in the account under the Exophones section  . 

![](https://api.archbee.com/api/optimize/SK2urIgCC2ml99YyqWOSl/3HACXHNXtH8iQfwF5col9_.blob)

SIP Exophones discussed later in the section refers to Exophones of Voip type as shown above.

**NOTE :- SIP and VOIP are used interchangeably in the document**

### Glossary

| Terminology         | Description                                                  |
| ------------------- | ------------------------------------------------------------ |
| App                 | Mobile application                                           |
| Application Backend | Customer backend service for the application                 |
| Client              | User / Subscriber / Client signing up to use the mobile app  |
| Customer            | Exotel’s customer licensing the SDK                          |
| Voip                | Voice over IP                                                |



## IOS SDK Integration

### Getting Started

### Software Package

Th&#x65;*&#x20;ExotelVoice* SDK software package includes 

- IOS *ExotelVoice.framework.zip* file of the SDK
- Integration Guide
- Sample application source code for reference: *exotel-voice-sample.zip*
- Doc for SDK API reference: *exotel-ios-voice-sample-doc.zip*
- Subscriber Management API Documentation: *Subscriber Management API*

### Add Exotel Voice SDK Library to Project

  	Configure ExotelVoice.Framework

- unzip  ExotelVoice.framework.zip
- Copy ExotelVoice.framework folder under your project root directory



### Target Platform

- Supported IOS Version: iOS 10+
- Supported IOS Sdk Arch : arm64 



### Permissions

- Record permission 
- Notification permission



Proguard Rules - Not applicable

### IOS Service

The application needs to integrate *ExotelVoice* in a service. This should be run as a foreground service when a call is in progress so that the application is not killed when it moves to the background. When the call is over, service should be moved to the background. Refer to *VoiceAppService* in the sample application.

## Initialize Library

ExotelVoice Interface Class provides an entry point to initialize the voice library and set it up for handling calls. 

| // Get Exotel Voice Client<br />exotelVoiceClient = ExotelVoiceClientSDK.getExotelVoiceClient()<br /><br />//set the event listener for sdk logs.<br />exotelVoiceClient?.setEventListener(eventListener: self)<br /><br />// Set listener to handle events from voice client <br />extension VoiceAppService: ExotelVoiceClientEventListener \{<br /><br />// Initialization success handler<br />public func onInitializationSuccess() \{ ... }<br /><br />// Initialization failure handler<br />public func onInitializationFailure(error: ExotelVoiceError) \{...}<br /><br />/// Indicates de-initialization of the SDK<br />public func onDeinitialized()\{...}<br /><br />// Logs reported by the voice library<br />public func onLog(level: LogLevel, tag: String, message: String) \{...}<br /><br /><br />// Log upload success handler<br />public func onUploadLogSuccess() \{...}<br /><br />// Log upload failure handler<br />public func onUploadLogFailure(error: ExotelVoiceError) \{...}<br /><br />// Authentication failure handler<br />public func onAuthenticationFailure(error: ExotelVoiceError) \{...}<br />} |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |



SDK initialization requires a *subscriberToken* generated by the Exotel platform. Workflow for this token generation and management is described in section **Authentication and Authorization**. 

| // Request token from application backend (example)<br />String subscriberToken = getAccessToken(); |
| --------------------------------------------------------------------------------------------------- |



The *subscriberToken* is provided to *ExotelVoice* during initialization. 

| // Initialize client library<br />let content = \["DeviceId": UIDevice.current.identifierForVendor?.uuidString,<br />                           "DeviceType": "ios"]<br /><br /><br />exotelVoiceClient?.initialize(<br />	context: content as \[String : Any],// iOS context<br />	hostname: hostname,               // Exotel Voice Platform Host URL<br />	subscriberName: subscriberName,   // To generate token for subscriber<br />	displayName: displayName,         // Display name of the subscriber<br />	accountSid: accountSid,           // Exotel Account SID <br />	subscriberToken: subscriberToken  // Token fetched from Exotel platform <br />) |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |



Client library notifies *onInitializationSuccess()* on success and *onInitializationFailure()* on failure. On expiry of *subscriberToken*, the library will post onAuthenticationFailure()  at which application should request new *subscriberToken* from application backend and program in the *initialize()* API.*&#xA0;*



| **Parameter**   | **Description**                                                                                                                                                                                           |
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Hostname        | *https\://miles.apac-sg.exotel.in/v2*                                                                                                                                                                     |
| SubscriberName  | Param “subscriber\_name” returned as part of the *Subscriber management API* to create a subscriber.                                                                                                      |
| DisplayName     | Subscriber name.                                                                                                                                                                                          |
| AccountSId      | *Account SID param from API settings page* in the Exotel Dashboard.                                                                                                                                       |
| SubscriberToken | can be gotten from the API in the \`Get Subscriber Token\` section in the *Subscriber Management API document.* In the *subscriberToken*, both the *refresh token* and *access token* are base64 encoded. |



&#x9;Subscriber token format example 

| "subscriber\_token":<br />\{ <br />"refresh\_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJleG90ZWwiLCJzdWIiOiJBcmNoaXQiLCJpYXQiOjE1NzY2NDc5OTksImV4cCI6MTU3Njc0Nzk5OSwiY2xpZW50X2lkIjoiNUUwNTg2NUUifQ.Hc3umVfFlKIPiJ8R9kcP9o9hE9he51le08rO22u7eqs",<br />"access\_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJleG90ZWwiLCJzdWIiOiJBcmNoaXQiLCJpYXQiOjE1NzY2NDc5OTksImV4cCI6MTU3Njc0Nzk5OSwic2NvcGUiOiJ2b2ljZSIsImNsaWVudF9pZCI6IjVFMDU4NjVFIn0.uQd\_aHGBIT4XqTPfl-567dmjQrKlmLtPag0KpGB2QMA"<br />} |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |



Below are the sample methods to get the subsriberToken, Customers can implement this in their own method.

Use the below HTTP APIs to get the subscriber token and convert subscriber token to jobject and pass it to exotelVoiceClient.initialize();

Note: 

- Copying manually generated subscriber token has issues. Best way is to make API calls in source code and pass them by converting to jobject to the exotelVoiceClient.initialize();
- Pass the **proper device ID** to the login api to avoid invalid refresh token error.
- Device\_id is the actual device id in which the app is running using below sample code. 

| "device\_id": UIDevice.current.identifierForVendor?.uuidString |
| -------------------------------------------------------------- |



**Generate access token:**

Refer “Subscriber Management API” documentation section 3.1 for creating subscriber token.

### Stopping SDK

&#x9;To de-initialize the sdk, ExotelVoiceClient  exposes stop api to de-initialize the SDK.

| exotelVoiceClient?.stop(); |
| -------------------------- |



when sdk gets de-initialized,

client SDK will send callback onDeinitialized()

### Managing Calls

After successful initialization, the library is ready to handle calls. Dialing-out calls or receiving incoming calls is managed through *CallController* Interface.

| // Get Call Controller interface<br />callController = self.exotelVoiceClient?.getCallController() |
| -------------------------------------------------------------------------------------------------- |



Call progress events are notified to the application through the *CallListener* Interface attached to the *CallController* object.  



| // Attach call listener<br />extension VoiceAppService: CallListener \{<br /><br />// Handler for incoming call alert<br />public func onIncomingCall(call: Call) \{ . . . }<br /><br /><br />// Handler for outgoing call initiated event<br />public func onCallInitiated(call: Call) \{ . . . }<br /><br /><br />// Handler for call ringing event for outgoing call<br />public func onCallRinging(call: Call) \{. . .}<br /><br /><br />// Handler for call connected event<br />public func onCallEstablished(call: Call) \{. . .}<br /><br /><br />// Handler for call termination event<br />public func onCallEnded(call: Call) \{. . .}<br /><br />// Handler for missed call alert <br />public func onMissedCall(remoteId: String, time: Date) \{. . .}  <br />} |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |



Each call object provides a Call Interface to manage the call lifecycle and perform operations like answer incoming calls, hangup calls, mute, unmute, and get call statistics.

| // Mute<br />mCall?.mute()<br /><br />// Terminate call<br />mCall?.hangup() |
| ---------------------------------------------------------------------------- |



### Make Outgoing Call

To make outgoing calls, the *CallController* interface must be created and a listener interface is attached as mentioned in section **Managing Calls**. Provide sip exophone number in *dial()* API of *CallController* Interface to make outgoing calls.

| // Get Call Controller interface<br />self.callController = self.exotelVoiceClient?.getCallController()<br /><br />// Attache call progress listener<br />self.callController?.setCallListener(callListener: self)<br /><br />// Dial out call to remoteId<br />call = callController?.dial(remoteID: \<value>, message: \<value>) |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |



**Params for dial method -&#xA0;**

| **Param** | **Mandatory** | **Description**                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| --------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| remoteID  | Yes           | ExotelVoice library always routes the call via SIP Exophone configured for your account. SIP Exophones are different from the PSTN / Mobile Exophones. They can be identified using sip: prefix. Contact *exotel support* to enable SIP Exophone in your account.<br /><br />Similar to the number masking workflow, the application backend must maintain the call context between caller and callee. On receiving the call at SIP Exophone, the exotel platform will request the callee details from the application backend to bridge the call. Refer section *Dial Whom Endpoint* for details.  |
| message   | No            | The string that will be passed here will be sent to the DialWhom Endpoint under the “CustomField” param. All alphanumeric characters are allowed along with comma \`,\` colon \`:\` and all kinds of brackets - \{}()\[]                                                                                                                                                                                                                                                                                                                                                                            |



Once initiated, call progress events can be monitored through *CallListener* Interface and call control operations can be performed using *Call* Interface. After dialing, the application can use *hangup()* API on the call object to cancel / terminate outgoing calls. 

| // Cancel / Terminate call<br />mCall?.hangup() |
| ----------------------------------------------- |



### Receive Incoming Call

The Mobile application receives incoming call data in Push Notification sent by your application backend. Refer section **Push Notification Endpoint** for details. 

![](https://api.archbee.com/api/optimize/SK2urIgCC2ml99YyqWOSl/aZQiL41XHkcnSy3o3I4q7_upload.png)

To handle the incoming call, the *ExotelVoice* library should be in an initialized state. Refer to section **Initialize Library**. After initialization, the CallController Interface should be created and the listener be attached as mentioned in section **Managing Calls**.



| // Get Call Controller interface<br />self.callController = self.exotelVoiceClient?.getCallController()<br /><br />// Attache call progress listener<br />self.callController?.setCallListener(callListener: self) |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |



The sample application uses Firebase Cloud Messaging to push call data to the device. Once received at the mobile app, it is sent to *ExotleVoiceClient* through *relaySessionData* API.



|  **extension** AppDelegate: UNUserNotificationCenterDelegate \{<br />**func** application(\_ application: UIApplication,<br />                     didReceiveRemoteNotification userInfo: \[AnyHashable: **Any**],<br />                     fetchCompletionHandler completionHandler: **@escaping** (UIBackgroundFetchResult)<br />                     -> Void) \{<br /><br />// decode json and build session data structure    <br />        sessionData\["payload"] = payload<br />                sessionData\["payloadVersion"] = payloadVersion<br />                sessionData\["subscriberName"] = userId<br /><br />        <br />        exotelVoiceClient?.relaySessionData(payload: sessionData)<br /><br />    <br />} |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |



The voice library will process and validate the data and send an *onIncomingCall()* event to the application with the call object. The application can call the *answer()* API to accept the incoming call.

| // Accept call<br />mCall?.answer() |
| ----------------------------------- |



The application can call *hangup()* API on the call object to decline an incoming call.

| // Decline / Terminate call<br />mCall?.hangup() |
| ------------------------------------------------ |



## Call Kit Integration


If your app is integrated with Call Kit, refer to the following *lifecycle diagram* to ensure there are no conflicts. 

**User Interaction**:

- On the left side, we see a user icon representing interactions with a sample app.
- The user initiates actions like “click dial” for outgoing calls and “accept call” or “reject call” for incoming ones.

**Pre-requisites :&#xA0;**

- **CallKit UUID&#x20;**&#x77;ill manage by your App. 

| private static var activeUUID: UUID? |
| ------------------------------------ |



- **CXProviderDelegate&#x20;**&#x69;s subscriber by App to handle the call kit actions

| let providerConfiguration = CXProviderConfiguration(localizedName: "Exotel Sample App")<br />providerConfiguration.maximumCallsPerCallGroup = 1<br />providerConfiguration.supportsVideo = false<br />providerConfiguration.supportedHandleTypes = \[.phoneNumber]<br />providerConfiguration.ringtoneSound = "Ringtone.aif"<br />if let iconImage = UIImage(named: "AppIcon") \{<br />    providerConfiguration.iconTemplateImageData = iconImage.pngData()<br />}<br />        <br />provider = CXProvider(configuration: providerConfiguration)<br />callController = CXCallController()<br />provideDelagete = ProviderDelegate(provider: provider!) <br />provider!.setDelegate(provideDelagete, queue: nil)<br /> |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |



Here **ProviderDelegate** is custom class which has implemented the **CXProviderDelegate.**

### Outgoing Calls  

Start outgoing calls by calling dial() api of exotel sdk. 

| callController?.dial(remoteID: destination, message: message) |
| ------------------------------------------------------------- |



Then after make sure to call the start Transaction with ***CXStartCallAction*** (using the UUID). 

| let handle = CXHandle(type: .phoneNumber, value: destination)<br />activeUUID = UUID()<br />let startCallAction = CXStartCallAction(call: activeUUID!, handle: handle)<br />startCallAction.isVideo = video<br /><br />let transaction = CXTransaction()<br />transaction.addAction(startCallAction)<br /><br />requestTransaction(transaction) |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |

1. Call-kit UI will start in background
2. Call kit delegate method for action **CXStartCallAction&#x20;**&#x77;ill get executed

| func provider(\_ provider: CXProvider, perform action: CXStartCallAction) \{<br />        <br />        setActiveUUID(uuid: action.callUUID)<br />        <br />        action.fulfill()<br />        <br />} |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |



When sdk send **onCallRinging()** callback to app, app will start call kit timer for ringing after updating the status to ringing state.

|  provider?.reportOutgoingCall(with: activeUUID!, startedConnectingAt: nil) |
| -------------------------------------------------------------------------- |



Timer will start in call kit UI.

### Incoming Calls 

When relay session data (push notification data) triggers an incoming call notification, the CallKit UI should be set up to display the incoming call with options to accept or rejec&#x74;**.**&#x55;sers should be provided with the option to accept the call. They can choose to accept the call using the function ***reportIncomingCall***.

| class func reportIncomingCall(uuid: UUID, handle: String, hasVideo: Bool = false, completion: ((Error?) -> Void)? = nil) \{<br />        // Construct a CXCallUpdate describing the incoming call, including the caller.<br />        let update = CXCallUpdate()<br />        update.remoteHandle = CXHandle(type: .phoneNumber, value: handle)<br />        update.hasVideo = hasVideo<br /><br />        // Report the incoming call to the system<br />        provider!.reportNewIncomingCall(with: uuid, update: update) \{ error in<br />            /\*<br />             Only add an incoming call to an app's list of calls if it's allowed, i.e., there is no error.<br />             Calls may be denied for various legitimate reasons. See CXErrorCodeIncomingCallError.<br />             \*/<br />            if let error = error \{<br />                VoiceAppLogger.error(TAG: CallKitUtils.TAG, message: "Error requesting transaction: \\(error)")<br />            } else \{<br />                VoiceAppLogger.info(TAG: CallKitUtils.TAG, message: "Requested transaction successfully:")<br />            }<br />        }<br />    }<br /> |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |

- Upon choosing to accept the call, the system should invoke the ***CXAnswerCallAction*** to answer the call.

| func provider(\_ provider: CXProvider, perform action: CXAnswerCallAction) \{<br />    VoiceAppLogger.info(TAG: TAG, message: "CXAnswerCallAction")<br />    VoiceAppService.shared.answer()<br />    // Signal to the system that the action was successfully performed.<br />    action.fulfill()<br />}    |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |

- Alternatively, users should also be provided with the option to reject the call. They can choose to reject the call using the ***CXEndCallAction***.

| func provider(\_ provider: CXProvider, perform action: CXEndCallAction) \{<br />    VoiceAppLogger.info(TAG: TAG, message: "CXEndCallAction")<br />    do \{<br />        try VoiceAppService.shared.hangup()<br />    } catch let error \{<br />        VoiceAppLogger.debug(TAG: TAG, message: "Error: \\(error.localizedDescription)")<br />    }<br />    // Signal to the system that the action was successfully performed.<br />    action.fulfill()<br />} |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |



**Connected Calls**:

- Once a call is established, its status is updated in the UI.
- The timer is started in the call kit once the call is connected.

**Ending Calls**:

- Both the sample app and the CallKit UI can initiate the action.
- When the user ends the call, the sample app starts a transaction with ***CXEndCallAction*** (using the UUID).
- Function *hangup()* is called.

| public func hangup() throws \{<br />    if (nil == mCall) \{<br />        let message = "Call Object is NULL"<br />        VoiceAppLogger.error(TAG: TAG, message: message)<br />        throw VoiceAppError(module: TAG, localizedDescription: message)<br />    }<br />    do \{<br />        try mCall?.hangup()<br />    } catch \{<br />        VoiceAppLogger.error(TAG: TAG, message: "Exception in call hangup with CallId: \\(String(describing: mCall?.getCallDetails().getCallId()))")<br />    }<br />} |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |



The home view updates after ending a call.

### PushKit Integration


PushKit is used to manage VoIP call notifications and handle incoming call events with CallKit. 

- **Initializing PushKit**

The PushKit registry is initialized inside the **AppDelegate** class within the ***didFinishLaunchingWithOptions*** method.

| var pushRegistry: PKPushRegistry = PKPushRegistry(queue: DispatchQueue.main) pushRegistry.delegate = self <br />pushRegistry.desiredPushTypes = \[.voIP] |
| -------------------------------------------------------------------------------------------------------------------------------------------------------- |



- **Implementing *PKPushRegistryDelegate*&#xA0;**

An extension of the ***AppDelegate*** class conforms to the ***PKPushRegistryDelegate*** protocol to handle PushKit-related events.



| extension AppDelegate: PKPushRegistryDelegate \{<br />    func pushRegistry(\_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) \{<br />        guard type == .voIP else \{ return }<br />        <br />      <br />    }<br /><br />    func pushRegistry(\_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) \{<br />             completion()<br />            <br />        }<br />}<br /> |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |



- **Receiving the PushKit Token&#x20;**&#xA;The ***pushRegistry(\_:didUpdate\:for:)*** method is triggered upon app launch or whenever the PushKit token changes. This token is unique for the device and must be sent to the backend.

&#x9;

| extension AppDelegate: PKPushRegistryDelegate \{<br />    func pushRegistry(\_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) \{<br />        guard type == .voIP else \{ return }<br /><br />        let token = pushCredentials.token.map \{ String(format: "%02x", $0) }.joined()<br />        if token.isEmpty \{<br />            VoiceAppLogger.debug(TAG: TAG, message: "PushKit Token is not generated yet!!")<br />            return<br />        }<br /><br />        VoiceAppLogger.debug(TAG: TAG, message: "PushKit token: \\(token)")<br />        UserDefaults.standard.set(token, forKey: UserDefaults.Keys.firebaseToken.rawValue)<br />    }<br />}<br /> |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |



- **Handling Incoming Push Notifications**
  The ***pushRegistry(\_:didReceiveIncomingPushWith\:for\:completion:)*** method is triggered when the app receives a PushKit notification. It processes the payload and triggers the CallKit UI to display the incoming call screen.

****

| func pushRegistry(\_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) \{<br />    VoiceAppLogger.debug(TAG: TAG, message: "VoIP push received")<br /><br />    ApplicationUtils.checkMicrophonePermission \{ isEnabled in<br />        guard isEnabled else \{<br />            UserDefaults.standard.set("false", forKey: UserDefaults.Keys.isLoggedIn.rawValue)<br />            completion()<br />            return<br />        }<br />    }<br /><br />    ApplicationUtils.checkNotificationsPermission \{ isEnabled in<br />        guard isEnabled else \{<br />            UserDefaults.standard.set("false", forKey: UserDefaults.Keys.isLoggedIn.rawValue)<br />            completion()<br />            return<br />        }<br />    }<br /><br />    guard let payloadDict = payload.dictionaryPayload as? \[String: Any],<br />          let callerId = payloadDict\["subscriberName"] as? String else \{<br />        VoiceAppLogger.error(TAG: TAG, message: "Invalid payload format or missing callerId")<br />        completion()<br />        return<br />    }<br /><br />    VoiceAppService.shared.pushNotificationData = payloadDict<br /><br />    let validateLogin = UserDefaults.standard.string(forKey: UserDefaults.Keys.isLoggedIn.rawValue) ?? "false"<br />    guard validateLogin != "false" else \{<br />        VoiceAppLogger.error(TAG: TAG, message: "User is not logged into App")<br />        completion()<br />        return<br />    }<br /><br />    CallKitUtils.displayIncomingCall(handle: callerId)<br />    completion()<br />}<br /> |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |



**Answering the Call&#x20;**&#xA;When the user accepts the call via the CallKit UI by tapping the accept button, the ***provider(\_:perform\:CXAnswerAllAction)*** method in the **CXProvider** delegate is triggered. This method initializes the audio session and call the sendPushNotificationData(). method.

| func provider(\_ provider: CXProvider, perform action: CXAnswerCallAction) \{<br />    VoiceAppLogger.info(TAG: TAG, message: "CXAnswerCallAction")<br /><br />    guard let payLoad = VoiceAppService.shared.pushNotificationData,<br />          let userId = payLoad\["subscriberName"] as? String,<br />          let payLoadVersion = payLoad\["payloadVersion"] as? String,<br />          let payLoadData = payLoad\["payload"] as? String else \{<br />        VoiceAppLogger.error(TAG: TAG, message: "Missing data in payload")<br />        return<br />    } <br /><br />    VoiceAppLogger.debug(TAG: TAG, message: "Handling the notification on answer button tap")<br />    self.startAudioSessionIfNeeded()<br /><br />    DispatchQueue.main.asyncAfter(deadline: .now()) \{<br />        VoiceAppService.shared.sendPushNotificationData(<br />            payload: payLoadData,<br />            payloadVersion: payLoadVersion,<br />            userId: userId<br />        )<br />    }<br /><br />    action.fulfill()<br />}<br /> |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |



**Activating the Audio Session&#x20;**&#xA;Calling the ***startAudioSessionIfNeeded()*** method initializes and activates the ***AVAudioSession*** to ensure it is correctly configured for voice call functionality. It uses the ***AVAudioSession*** framework to manage audio behaviors.. 

| func startAudioSessionIfNeeded() \{<br />    do \{<br />        let session = AVAudioSession.sharedInstance()<br />        try session.setCategory(.playAndRecord, mode: .voiceChat, options: \[.duckOthers, .allowBluetooth, .defaultToSpeaker])<br />        try session.setActive(true)<br />    } catch \{<br />        VoiceAppLogger.error(TAG: TAG, message: "Error starting audio session: \\(error)")<br />    }<br />}<br /><br /> |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |



**Incoming Call Management**
After receiving an incoming call notification and displaying the CallKit UI, the app handles the call by playing a ringtone and invoking the **answer()** method to accept the call. This is managed within the ***onIncomingCall*** function.****


| public func onIncomingCall(call: Call) \{<br />    VoiceAppLogger.debug(TAG: TAG, message: "Incoming Call Received, CallId: \\(call.getCallDetails().getCallId()) remoteId: \\(call.getCallDetails().getRemoteId())")<br /><br />    tonePlayback.playRingTone()<br />    mCall = call<br /><br />    DispatchQueue.main.asyncAfter(deadline: .now() + 1) \{<br />        self.answer()<br />    }<br />}<br /><br /> |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |



### Audio Management

ExotelVoice can detect the change in audio output selection. Default audio output mode is the earpiece. When a wired headset is plugged in or a Bluetooth headset is paired with a phone, it automatically routes audio over a new route. Speaker phone mode can be enabled using *Call* object APIs. 

| // Enable speaker mode<br />mCall?.enableSpeaker()<br /><br />// Disable speaker mode<br />mCall?.disableSpeaker() |
| ------------------------------------------------------------------------------------------------------------------ |



Local mic can be muted and unmuted during in-call using *Call* object APIs.

| // Enable speaker mode<br />mCall?.mute()<br /><br />// Disable speaker mode<br />mCall?.unmute() |
| ------------------------------------------------------------------------------------------------- |



Bluetooth mode can be enabled and disabled using *Call* object APIs. This will only work when any bluetooth device is connected to the phone. 

| /// Enables bluetooth mode for the call<br />mCall?.enableBluetooth()<br />    <br />/// Disables bluetooth mode for the call<br />mCall?.disableBluetooth() |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------ |



### Call State

The Call State can be accessed by calling *getLatestCallDetails().getCallState()*. This state can be displayed on the calling screen to let the user know about the state of the call. Refer to the *Call State Flow diagram*.



| **Call State**      | **Description**                                                                                                                                                                                                                                                                                                                                                                                                                                                                            | **Call Back**       | **Recommended message to display on the calling screen** |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------- | -------------------------------------------------------- |
| NONE                | This state occurs when a call object is created but not yet initiated.                                                                                                                                                                                                                                                                                                                                                                                                                     | -                   | -                                                        |
| OUTGOING\_INITIATED | This state is set when an outgoing call is initiated. It’s set after dialing and before the call starts ringing.                                                                                                                                                                                                                                                                                                                                                                           | -                   | Connecting                                               |
| EARLY               | Indicates early media reception before the call is answered.                                                                                                                                                                                                                                                                                                                                                                                                                               | -                   | -                                                        |
| RINGING             | The receiver’s phone is ringing but hasn’t been answered yet.                                                                                                                                                                                                                                                                                                                                                                                                                              | onCallRinging()     | Ringing                                                  |
| CONNECTING          | The call starts connecting.                                                                                                                                                                                                                                                                                                                                                                                                                                                                | -                   | Connecting                                               |
| INCOMING            | This state is set when there’s an incoming call, and the system isn’t idle.                                                                                                                                                                                                                                                                                                                                                                                                                | onIncomingCall()    | Caller ID, custom information related to the callee      |
| ANSWERING           | Occurs when the user accepts the incoming call; it’s a transitional phase before the call gets established.                                                                                                                                                                                                                                                                                                                                                                                | -                   | Answering                                                |
| ESTABLISHED         | Indicates that both parties have accepted the call, and communication is established.                                                                                                                                                                                                                                                                                                                                                                                                      | onCallEstablished() | Connected                                                |
| MEDIA\_DISRUPTED    | Occurs if there’s a disruption in media transmission during an ongoing call due to issues like RTP loss. <br />To handle such disruptions effectively ensuring continuity or termination based on severity it initiates reconnection procedures and moves to RECONNECTING if RTP loss persists. <br />To ensure calls remain active during temporary disruptions it monitors RTP resumption or port renewal activities and returns to ESTABLISHED upon successful media restoration.<br /> | onMediaDisrupted()  | Reconnecting                                             |
| RENEWING\_MEDIA     | This state indicates that media (RTP) is resuming or renewing after being disrupted.                                                                                                                                                                                                                                                                                                                                                                                                       | onRenewingMedia()   | Reconnecting                                             |
| ENDING              | The process of ending or dropping an ongoing or established call starts here                                                                                                                                                                                                                                                                                                                                                                                                               | -                   | -                                                        |



### Call Statistics

Application can query the call quality statistics periodically during the call by using *getStatistics()* API of the *Call* object. It provides info about codec and the call QoS parameters like packet loss, jitter and round trip delay.

### Call Details

Application can query the call details record of the call using *getCallDetails()* API of the *Call&#x20;*&#x69;nterface. The CDR provides info on callId, call state, call duration, call end reason etc. This API can be queried only for the latest call since SDK maintains only a single call context.

### Handling of PSTN calls

*ExotelVoice* has following behavior during PSTN calls:

- When a PSTN call is in progress, no outgoing calls are allowed by the voice client.
- When a PSTN call is in progress, any incoming VoIP call is automatically declined by the voice library and reported as a missed call to the application. 
- When a VoIP call is in progress and a PSTN call lands on the phone, then the VoIP call continues if the user declines or does not respond to the PSTN call. If the user accepts the PSTN call, then the VoIP call is automatically terminated by the voice client.

Handling of PSTN calls when Call Kit is Integrated

*ExotelVoice* has following behavior during PSTN calls:

- When a PSTN call is in progress, no outgoing calls are allowed by the voice client.
- The following scenarios will the handled by the Call Kit instead of the SDK - 
  - When a PSTN call is in progress and a VoIP call is incoming
  - When a VoIP call is in progress and a PSTN call lands on the phone


For these scenarios, Call kit provides options to the end user to either reject and accept the incoming call or hold and accept the incoming call.

### Reporting Problems

The voice client library logs are stored in the application internal storage. Any issue along with the logs can be reported by app to Exotel using *uploadLogs()* API of *ExotelVoice&#x20;*&#x49;nterface. 

| // Upload logs with description from startDate and endDate<br />exotelVoiceClient?.uploadLogs(startDate: startDate, endDate: endDate, description: description) |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------- |



The API triggers *onUploadLogSuccess()* or *onUpLoadLogFailure()* callback based on the success or failure of the operation.

### Reporting Call Quality Feedback

Call quality can be queried from the user and reported to the exotel platform at the end of the call.

| // Upload logs with description from startDate and endDate<br />int rating = 3<br />CallIssue issue = BACKGROUND\_NOISE<br />mPreviousCall?.postFeedback(rating: rating, issue: issue) |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |



The rating is a quality score that can take values 1 to 5.

| 5 - Excellent quality. No issues.<br />4 - Good quality. Negligible issues.<br />3 - Average quality. Minor audio noise. <br />2 - Bad quality. Frequent choppy audio or high audio delay.<br />1 - Terrible. Unable to communicate. Call drop, No-audio or one-way audio.  |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |



The call issue is a descriptive explanation of the issue.

| NO\_ISSUE         | No issues observed                            |
| ----------------- | --------------------------------------------- |
| BACKGROUND\_NOISE | Low audio clarity due to noisy audio          |
| CHOPPY\_AUDIO     | Frequent breaks in audio or garbling in audio |
| HIGH\_LATENCY     | Significant delay in audio                    |
| NO\_AUDIO         | No audio received from far user               |
| ECHO              | Echo during the call                          |



## Platform Integration

### Customer API Endpoints

Exotel provides full control to customers to implement call routing business logic. For this, customers need to host the following HTTP endpoints to handle callbacks from Exotel platform.

### Dial Whom Endpoint 

Host a HTTP endpoint which will be queried by the Exotel platform to get to the destination user.

**Method:** `GET`

**Request URL (Example):&#x20;**`https://company.com/v1/accounts/exotel/dialtonumber`

Note: This URL needs to be provided to Exotel or configured in the connect applet.

**Request Body**:

- CallSid  - unique call identifier
- CallFrom - caller username          
- CallTo - exophone
- CustomField - It will be passed only if you have sent the second optional param while making the call from the app. 

**Expected Response**:

On success, the API should return with response code 200 OK. The response body should be a string of type *sip:\<remoteId>*. “sip:” tag is needed to hint the exotel platform to connect calls over voip. At present, SIP and PSTN intermixing is not supported. Any other response is treated as failure. remoteId is the username with which the call destination subscriber was registered with Exotel platform. 

**Example:**

Request

| GET /v1/accounts/exotelip2ipcalling1/dialtonumber?CallSid=743ddcf5a0552050bc37b6d0ff9613bn\&CallFrom=sip\:Alice\&CallTo=sip:08040408080\&CallStatus=ringing\&Direction=incoming\&Created=Sat,+23+Nov+2019+22:00:37\&DialCallDuration=0\&StartTime=2019-11-23+22:00:37\&EndTime=1970-01-01+05:30:00\&CallType=call-attempt\&DialWhomNumber=\&flow\_id=249196\&tenant\_id=113828\&From=sip\:Alice\&To=sip:08040408080\&CurrentTime=2019-11-23+22:00:37 |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |



Response

| \{<br />    "fetch\_after\_attempt": false,<br />    "destination": \{<br />        "numbers": \[<br />            "sip:1234567890"<br />        ]<br />    },<br />    "outgoing\_phone\_number": "08080808080",<br />    "record": false,<br />    "recording\_channels": "dual",<br />    "max\_ringing\_duration": 30,<br />    "max\_conversation\_duration": 3600,<br />    "request\_id": "2e48100e6b474714b1a64bfa9f5b7a55",<br />    "method": "GET",<br />    "http\_code": 200,<br />    "code": null,<br />    "error\_data": null,<br />    "status": null<br />}<br /> |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |

&#x9;

### Push Notification Endpoint 

Exotel implements registration-less dialing where a user need not periodically register with SIP registrar for receiving incoming calls. Instead the call details are pushed to the device as push notification using which call can be established. This method is beneficial as calls will reach the user even when the app is not in foreground or swipe killed. It saves battery since it does not need to keep persistent voip connection when idle.

Exotel will provide call data to this endpoint that needs to be pushed to the client device from your application backend. 

Note - Headers are not supported in the notify endpoints.

**Method:** `POST`

**Request URL (Example):&#x20;**`https://<your_api_key>:<your_api_token>@company.com/v1/accounts/exotel/pushtoclient`

Note: This URL needs to be configured in Exotel platform

**Request Body**:

- subscriberName - Name with which subscriber registered with Exotel platform
- payload - call data which should be passed to the client SDK          
- payloadVersion - version of payload scheme

**Expected Response**:

On success, the API should return with response code 200 OK. Any other response is treated as failure.

## Exotel API Endpoints

### Subscriber Management

Customers can manage their subscribers in the Exotel platform using the APIs listed *“Subscriber-Management-API”* document. 

For clients to be able to use voip calling features they need to be added as subscribers under your account. There are two ways in which your client registration with Exotel account happens,

Pre-provisioning 

The customer pre-configures the client accounts even before the app is installed by the client. Refer to the *“Subscriber-Management-API”* document for details on creating client accounts.

Dynamic provisioning

A client account is created after the app is installed by the user and signs-up with the application backend. Refer to **Authentication and Authorization** for workflow details. Once client provisioning happens, clients can be managed using *Subscriber Management APIs*.



### Authentication and Authorisation

Access to the Exotel platform by *ExotelVoice* is authenticated using a set of bearer tokens - *subscriber\_token*. The Application backend must obtain these tokens from the Exotel platform and provide them to the client on request. Refer to the *“Subscriber-Management-API”* document for details.

![](https://api.archbee.com/api/optimize/SK2urIgCC2ml99YyqWOSl/66w0pcGOu6u72IEdmUd5U_.blob)



On receiving the *onAuthenticationFailure&#x20;*&#x65;vent, the application should request a new *subscriber\_token* from its backend and reinitialize the SDK as shown in section **Initialize Library**. Contact **exotel support** if you get *onAuthenticationFailure* even after token renewal.

*onAuthenticationError()&#x20;*&#x65;vent provides following error types: 

- AUTHENTICATION\_INVALID\_TOKEN - token parameters are invalid
- AUTHENTICATION\_EXPIRED\_TOKEN - token expired

Reference Documents

- IOS JavaDoc for th&#x65;*&#x20;ExotelVoice&#x20;*&#x6C;ibrary API
- *Subscriber-Management-API* document for details on API to manage  subscriber

### Support Contact

Please write to *hello\@exotel.in* for any support required with integration.

## Troubleshooting Guide

### Outgoing Call

I am not getting any requests on the “Dial Whom” endpoint for outgoing calls?

| 1. Check if *“Dial Whom”* URL was configured in **Connect Applet** and reachable.
2. Check if the call is listed in your account dashboard. If not, then 
   - Check if the phone has internet connectivity.
   - Check if the SIP Exophone number was set in *CallController\::dial()* API.
   - Check if C*allListener\::onCallFailed()&#x20;*&#x65;vent was received with error type CONGESTION\_ERROR, which means rate limit quota was exceeded.
   - Check if *CallListener\::onCallInitiated()&#x20;*&#x65;vent was received from the SDK. 
   - Check if *Call\::getCallDetails()* returns non-null *sessionId* field
   - Check if 
3. Report the issue to **exotel support** with *sessionId&#x20;*(from App) an&#x64;*&#x20;Call-Reference-Id&#x20;*(if available, from account dashboard) |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |



I am hearing a ringing tone but the callee has not received the call?

| Ringing tone implies that the call has reached the exotel platform.  |
| -------------------------------------------------------------------- |



### Incoming Call

My calls are not landing on the callee app?

| 1. Check if the call is listed in your account dashboard and dialout happened to the remote subscriber. If not then, refer to the troubleshooting guide for outgoing calls.
2. Check if the application received push notification with call payload from your application backend. If not, then
   - Check the device logs
   - Check if **Push Notification URL** is reachable and configured in the exotel platform.
   - Check if Push Notification URL got the call payload from exotel platform.
   - Check if the call payload was sent to the target device from your backend. 
3. If the call notification is received in the app, then 
   - Check if the library is initialized using *ExotelVoiceClient?.isInitialized()*
   - Check if payload received in push notification was programmed in the library through *ExotelVoiceClient?.relaySessionData()*
4. Report the issue to **exotel support** with *Call-Reference-Id&#x20;*(from account dashboard)  |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |



### Authentication

Requesting subscriber-token from exotel platform giving 4xx response?

| Response Code | Troubleshooting                                                                                                       |
| ------------- | --------------------------------------------------------------------------------------------------------------------- |
| 400           | Malformed packet. Check subscriber\_name format. It should be an alphanumeric string of size less than 16 characters. |
| 401           | It requires basic authentication using API key\:token                                                                 |
| 403           | VoIP calling is not enabled in your account. Contact exotel support.                                                  |



Why is SDK reporting *onAuthenticationFailure* response for a subscriber token received from exotel platform?

| 1. Check the *ErrorType* in response. If it is,                                 |
| ------------------------------------------------------------------------------- |
| - AUTHENTICATION\_INVALID\_TOKEN<br />Invalid Token. <br />Initialize new token |
| * AUTHENTICATION\_EXPIRED\_TOKEN<br />Token expired. Initialize new token       |
| Contact [Exotel support ](https://hello@exotel.com) if the issue persists.      |

