Skip to main content
PushKit allows iOS to wake your app — even from a terminated state — when a VoIP push notification arrives. This is required for reliable incoming call delivery on iOS.
1

Enable Push Notifications on your Bundle ID

Log in to the Apple Developer portal and open your app’s Bundle ID. Ensure that Push Notifications is checked under the list of capabilities.
2

Enable Voice over IP capability in Xcode

Open your project in Xcode and go to:Xcode Project → Your Target → Signing & CapabilitiesClick + Capability and add Background Modes. Then check the Voice over IP option.
3

Create a VoIP Services Certificate

In the Apple Developer portal, go to Certificates, Identifiers & Profiles → Certificates → Add and select VoIP Services Certificate.After generating it, download the .cer file and double-click it to install it into Keychain Access.Once installed, open Keychain Access, find the certificate, right-click it, and choose Export to save it as a .p12 file.
4

Convert the certificate to .pem format

With your exported .p12 file, run the following command in your terminal to convert it to a .pem file that can be used for sending VoIP pushes:
openssl pkcs12 -in YOUR_CERTIFICATES.p12 -out VOIP.pem -nodes -clcerts
Keep VOIP.pem somewhere safe — you will use it when sending test pushes.
5

Configure VoIP push handling in AppDelegate.swift

Your AppDelegate must implement PKPushRegistryDelegate to receive VoIP pushes and forward them to the plugin. The key delegate methods are:
// Handle updated push credentials — forward the token to the plugin
func pushRegistry(_ registry: PKPushRegistry, didUpdate credentials: PKPushCredentials, for type: PKPushType) {
    print(credentials.token)
    let deviceToken = credentials.token.map { String(format: "%02x", $0) }.joined()
    // Save deviceToken to your server
    SwiftFlutterCallkitIncomingPlugin.sharedInstance?.setDevicePushTokenVoIP(deviceToken)
}

func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenFor type: PKPushType) {
    print("didInvalidatePushTokenFor")
    SwiftFlutterCallkitIncomingPlugin.sharedInstance?.setDevicePushTokenVoIP("")
}

// Handle an incoming VoIP push — show the call UI
func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
    print("didReceiveIncomingPushWith")
    guard type == .voIP else { return }

    let id = payload.dictionaryPayload["id"] as? String ?? ""
    let nameCaller = payload.dictionaryPayload["nameCaller"] as? String ?? ""
    let handle = payload.dictionaryPayload["handle"] as? String ?? ""
    let isVideo = payload.dictionaryPayload["isVideo"] as? Bool ?? false

    let data = flutter_callkit_incoming.Data(id: id, nameCaller: nameCaller, handle: handle, type: isVideo ? 1 : 0)
    // Set additional fields as needed:
    // data.extra = ["user": "abc@123", "platform": "ios"]
    SwiftFlutterCallkitIncomingPlugin.sharedInstance?.showCallkitIncoming(data, fromPushKit: true) {
        completion()
    }
}
You must call completion() at the end of pushRegistry(_:didReceiveIncomingPushWith:for:completion:). Failing to call completion() causes iOS to terminate your app after repeated violations. If there is async work before showing the call, you can delay it slightly: DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { completion() }.
6

Retrieve the device push token in Dart

After the app launches and the PKPushRegistry delegate fires, the token is stored by the plugin and can be retrieved from Dart:
await FlutterCallkitIncoming.getDevicePushTokenVoIP();
Example output:
d6a77ca80c5f09f87f353cdd328ec8d7d34e92eb108d046c91906f27f54949cd
Send this token to your server so it can target the device when initiating a VoIP call.
The token is only available after SwiftFlutterCallkitIncomingPlugin.sharedInstance?.setDevicePushTokenVoIP(deviceToken) has been called inside pushRegistry(_:didUpdate:for:) in AppDelegate.swift.
7

Test VoIP push delivery

You can test VoIP push delivery using the curl command or a dedicated Mac app.
curl -v \
-d '{"aps":{"alert":"Hien Nguyen Call"},"id":"44d915e1-5ff4-4bed-bf13-c423048ec97a","nameCaller":"Hien Nguyen","handle":"0123456789","isVideo":true}' \
-H "apns-topic: com.hiennv.testing.voip" \
-H "apns-push-type: voip" \
--http2 \
--cert VOIP.pem:'<passphrase>' \
https://api.development.push.apple.com/3/device/<device token>
Replace com.hiennv.testing.voip with your app’s bundle ID suffixed with .voip, and replace <device token> with the token retrieved in the previous step.
To test VoIP push delivery from a terminated app state, configure your Xcode scheme: Edit Scheme → Run → Launch → Wait for the executable to be launched. This prevents Xcode from automatically foregrounding the app, letting the push wake it instead.

Build docs developers (and LLMs) love