Skip to content

How to Send and Handle VoIP Push Notifications in iOS

What are VoIP Push Notifications

A Voice over Internet Protocol (VoIP) app lets the user make and receive phone calls using an Internet connection instead of the device’s cellular service.

In the past, a VoIP app had to maintain a persistent network connection with a server to receive incoming calls and other data. This meant sending periodic messages back and forth between your app and the server to keep a connection alive, which resulted in frequent device wakes that wasted energy. And if a user quit your VoIP app, he could no longer receive the incoming call.

Since iOS 8, Apple introduced VoIP push notifications which can wake your app and pass across information about incoming calls. VoIP push notifications are differ from standard push notifications.

  1. Your app need to use PushKit Framework to register and receive VoIP push notifications.
  2. VoIP push notifications are considered high-priority notifications and are delivered without delay.
  3. VoIP push notifications can include more data than all other push notifications.For VoIP push notifications,the maximum payload size is 5 KB (5120 bytes).For all other remote notifications, the maximum payload size is 4 KB (4096 bytes).
  4. Your app is automatically relaunched if it’s not running when a VoIP push notifications is received.
  5. Your app is given runtime to process a push, even if your app is operating in the background.
  6. For apps built using the iOS 13 SDK or later, PushKit requires you to use CallKit when handling VoIP calls. CallKit ensures that all apps providing call-related services work seamlessly together on the user’s device, and respect features like Do Not Disturb. CallKit also operates the system’s call-related UIs, including the incoming or outgoing call screens.

Prepare Your App to Receive VoIP Push Notifications

Enable the VoIP Background Mode in Your App

Enable background mode in the Xcode Project > Main Target > Signing & Capabilities panel. Select the checkbox for Voice over IP.

Enabling the VoIP background mode in an app

Register for VoIP Push Notifications Using PushKit in Your App

swift
// Link to the PushKit framework
import PushKit
 
class AppDelegate: NSObject, UIApplicationDelegate {
   // Trigger VoIP registration on launch
   func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
      self.voipRegistration()
      return true
   }
 
   // Register for VoIP notifications
   func voipRegistration {
      let mainQueue = DispatchQueue.main
      // Create a push registry object
      let voipRegistry: PKPushRegistry = PKPushRegistry(queue: mainQueue)
      // Set the registry's delegate to self
      voipRegistry.delegate = self
      // Set the push type to VoIP
      voipRegistry.desiredPushTypes = [.voIP]
   }
}

Handle Updated Push Notification Credentials (Device Tokens)

VoIP push notification device token is different from standard push notification devices, so you need to use the below PKPushRegistryDelegate method to receive VoIP pushs token and send the token to your push server.

swift
extension AppDelegate: PKPushRegistryDelegate {
   // Handle updated push credentials
   func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {
      let deviceTokenString = pushCredentials.token.reduce("", { $0 + String(format: "%02X", $1) })
      print("VoIP device token: \(deviceTokenString)")
      // Register VoIP push token (a property of PKPushCredentials) with server
   }

}

Send VoIP Push Notifications From Your Server

Authentication

You can use either token-based authenticaiton or certificate-based authentication to send VoIP push notifications.

And, if you want to use certificate-based authenticaiton to send VoIP push notifications, you need to create a certificate that supports VoIP push notifications.

Request Headers

Request HeadersValue
apns-push-typevoip
apns-priority10
apns-topicbundle ID + .voip

In order to send VoIP push notifications, you need set apns-push-type to voip, set apns-priority to 10, and set apns-topic to your app bundle ID with .voip suffix.

Payload

VoIP push notificaiton payload need include information about the incoming call. For example, include the unique call identifier that your server uses to track the call. Your app can use this identifier to check in with the server later. You might also want to include information about the caller, so that you can display that information in the incoming call UI.

The maximum payload size of VoIP push notificaitons is 5 KB (5120 bytes).

json
{
   "aps" : {
      "alert":"VoIP Comming"
   },
   "handle" : "[email protected]",
   "callUUID" : "db1dff98-bfef-4ec0-8ac6-8187a3b0c645" 
}

Send Test VoIP Push Notifications Using APNsPush

With all these information, you can try to send test VoIP push notifications using ApnsPush Test Tool.

Handle VoIP Push Notifications in Your App

For VoIP push notifications, the system launches or wakes your app and delivers the notification to your app’s PKPushRegistry object, which calls the pushRegistry(_:didReceiveIncomingPushWith:for:completion:) method of its delegate. Use that method to display the incoming call UI and to establish a connection to your VoIP server.

Create a Call Provider Object to Manage Calls in Your App

VoIP apps must use CallKit to present the system’s call-related interfaces. You present these interfaces using a CXProvider object, which manages user interactions for both incoming and outgoing calls. Create a provider object early in your app’s life cycle and make it available to your app’s call-related code.

swift
import PushKit
import CallKit

class AppDelegate: NSObject, UIApplicationDelegate {
   var callProvider :CXProvider?

   // Trigger VoIP registration on launch
   func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
      self.voipRegistration()
      self.createCallProvider()
      return true
   }
 
   // Register for VoIP notifications
   func voipRegistration {
      let mainQueue = DispatchQueue.main
      // Create a push registry object
      let voipRegistry: PKPushRegistry = PKPushRegistry(queue: mainQueue)
      // Set the registry's delegate to self
      voipRegistry.delegate = self
      // Set the push type to VoIP
      voipRegistry.desiredPushTypes = [.voIP]
   }

    func createCallProvider() {
        // Configure the app's CallKit provider object.
        let config  = CXProviderConfiguration()
        config.supportsVideo = true
        config.supportedHandleTypes = [.generic]
        config.maximumCallsPerCallGroup = 1
        
        // Create the provider and attach the custom delegate object
        // used by the app to respond to updates.
        
        callProvider = CXProvider(configuration: config)
        callProvider?.setDelegate(self, queue: nil)
    }

}

extension AppDelegate: CXProviderDelegate {
   /// Called when the provider has been reset. Delegates must respond to this callback by cleaning up all internal call state (disconnecting communication channels, releasing network resources, etc.). This callback can be treated as a request to end all calls without the need to respond to any actions
    func providerDidReset(_ provider: CXProvider) {
        print("Function: \(#function) called")
    }

    //optional delegate methods

}

Respond to VoIP Push Notifications in Your App

The following code example shows how you might process an incoming VoIP push notification in your pushRegistry(_:didReceiveIncomingPushWith:for:completion:) method.

After extracting the call data from the notification’s payload dictionary, create a CXCallUpdate object and pass it to the reportNewIncomingCall(with:update:completion:) method of your app’s CXProvider object.

While CallKit processes your request, establish a connection to your VoIP server in parallel; you can always notify CallKit later if you run into problems.

swift
extension AppDelegate: PKPushRegistryDelegate {
    func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {
        let deviceTokenString = pushCredentials.token.reduce("", { $0 + String(format: "%02X", $1) })
        print("VoIP device token: \(deviceTokenString)")
        // Register VoIP push token (a property of PKPushCredentials) with server
    }
        
    // Handle incoming pushes
    func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
        if type == .voIP {
            // Extract the call information from the push notification payload
            if let handle = payload.dictionaryPayload["handle"] as? String,
               let uuidString = payload.dictionaryPayload["callUUID"] as? String,
                let callUUID = UUID(uuidString: uuidString) {
                // Configure the call information data structures.
                
                let callUpdate = CXCallUpdate()
                let phoneNumber = CXHandle(type: .phoneNumber, value: handle)
                callUpdate.remoteHandle = phoneNumber
                callUpdate.hasVideo = false
                callUpdate.supportsGrouping = false
                callUpdate.supportsHolding = false
                callUpdate.supportsDTMF = false
                
                // Report the call to CallKit, and let it display the call UI.
                callProvider?.reportNewIncomingCall(with: callUUID, update: callUpdate, completion: { (error) in
                    if error == nil {
                        // If the system allows the call to proceed, make a data record for it.
                        // let newCall = VoipCall(callUUID, phoneNumber: phoneNumber)
                        // self.callManager.addCall(newCall)
                    }
                    // Tell PushKit that the notification is handled.
                    completion()
                })
                
            }
            // Asynchronously register with the telephony server and
            // process the call. Report updates to CallKit as needed.
            
            // establishConnection(for: callUUID)
        }
    }
    
    func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenFor type: PKPushType) {
        // Notify your push server to invalid VoIP push token 
    }
}

If the system allows your call to proceed, the reportNewIncomingCall(with:update:completion:) method executes its completion block and CallKit displays the incoming call interface. At that point, use the delegate of your CXProvider object to respond to user interactions with the interface. For example, use your delegate to respond when the user answers or ends the call.


Have any questions? Feel free to drop me a message on Twitter!

References