Questions about VoIP Push compliance rules and CallKit handling

Hello everyone,

I’m an iOS developer working on a real-time communication app that supports VoIP calls using CallKit. The app has been in production for more than 5 years.

Over the years, some users have occasionally reported that they do not receive incoming call pushes. We have tried multiple optimizations on both the client and server side, but the improvement has been limited.

From Apple documentation and discussions online, I understand that iOS may restrict VoIP pushes if the system detects violations of VoIP push usage rules (for example, not presenting a CallKit call after receiving a VoIP push). However, the exact rules and thresholds for these violations are not clearly documented, so I’d like to ask a few questions to better understand the expected behavior.

Below is a simplified description of our current call flow. Call Flow Caller

When the user initiates a call:

We do not use CallKit

The call is handled entirely using a custom in-app call UI

Callee

When the user receives a call:

  1. Device locked or app in background

A VoIP push wakes the app

The app presents the CallKit incoming call UI

  1. App in foreground

The server still sends a VoIP push

The app first reports the call to CallKit

After a very short delay, the app programmatically ends the CallKit call

Then a custom in-app call UI is presented via the app's long connection

The reason we always send a VoIP push (even when the app is in the foreground) is that we want to maximize call delivery reliability.

We do not use CallKit. The call is handled entirely using a custom in-app call UI.

This is a bad idea. It's going to significantly complicate your app’s audio logic and, most practically, it means that ANY other incoming call from ANY CallKit app will IMMEDIATELY interrupt your app’s audio. It's also going to allow a variety of other odd edge cases which will be similarly disruptive (for example, now playing activity can interrupt your calls). The PRIMARY reason CallKit was originally created was so the system could properly arbitrate and manage the audio of VoIP apps, preventing all those edge cases.

However, the exact rules and thresholds for these violations are not clearly documented, so I’d like to ask a few questions to better understand the expected behavior.

The basic policy rule is that your app is not required to report a call if it either:

  1. Is already on a call.

  2. Is in the foreground.

However, in practice, both of those criteria are much harder to implement than they seem. The problem here is that your app is managed by callservicesd, so what actually matters is what state IT thinks your app is in, NOT what state YOUR app thinks it's in.

Going forward (iOS 26.4+), we've introduced an API that "properly" addresses these issues. The details are in this forum post, but the new API formally tells your app whether or not you must report a call. Note that this new delegate both addresses the two cases above and will also allow you to discard pushes which are "old enough" that they're unlikely to still be valid. I'll talk about your options on older systems below, but you should also adopt the new delegate as soon as possible and rely on its guidance instead. Note that the old delegate will be ignored on older systems, so you can include it "now" without worrying about it changing anything on your older systems.

Shifting to older systems:

If you're already on a call (#1), then the right way to handle this case is to report a new call using the UUID of your existing call. In that case, one of two things will happen:

  1. If you're still on a call, then your new call report will fail with a duplicate UUID error. Nothing will be visible to the user, nor will your app be "penalized" or otherwise disrupted.

  2. If the existing call happens to have ended, then your new call report will occur, which you'll then have to end. This does mean the call will be briefly visible to the user, but that's better than crashing.

Critically, the flow above is entirely "safe", as both cases meet the requirements of our API contract.

Shifting to foreground handling, there isn't any approach that I can guarantee will be safe. Just like call changes, app transitions are asynchronous, so it's entirely possible for your app to be "in the background" but not yet "know" that's the case. All I can really suggest here is that you use the main thread for push delivery (not a background thread) and that you minimize main thread activity. Together, that minimizes the opportunity that your app’s state will be out of date, which is the best you can do to reduce the risk of being terminated.

thresholds for these violations are not clearly documented

We've never formally documented the exact threshold, but if you experiment a bit, you'll find that delivery will stop after ~5 reporting failures within ~24 hours. Note that deleting and reinstalling the app will reset the limit if you want to test this. Also, the debugger disables this termination logic, so you can test this with Xcode.

__
Kevin Elliott

Additional questions:

1. What does “timely” handling of VoIP pushes mean?

After receiving a VoIP push in:

-(void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(PKPushType)type withCompletionHandler:(void (^)(void))completion;

If the app does not promptly present a CallKit incoming call or does not call the completion handler in time, it may be considered a violation.

My questions are:

What exactly counts as “timely”?

Does CallKit need to be reported within the same runloop, or within a specific time window (for example, within 1 second)?

Does the completion handler also have a similar timing requirement?

2. Cold launch delay

When the app is not running, a VoIP push will cause the system to launch the app before calling didReceiveIncomingPushWithPayload.

If the app launch process is slow, there may be a long delay between app launch and the push callback.

Could this delay potentially cause the system to treat the push as improperly handled, resulting in a VoIP push restriction?

3. Ending CallKit quickly when the app is in the foreground

When the app is already in the foreground, our current flow is:

Receive VoIP push

Report the call to CallKit

After a very short delay (around 0.5 seconds), programmatically end the CallKit call

Present the custom in-app call UI

Could this behavior potentially be considered misuse of VoIP push or CallKit by the system?

4. Recovery after VoIP push restriction

If the system determines that the app has violated VoIP push rules and VoIP pushes stop arriving, the only workaround we are currently aware of is:

The user uninstalls and reinstalls the app

Are there any other ways to recover from this state while preserving user data?

Also, if the user does nothing, will the system automatically remove the restriction after some time?

Thanks!

My questions are: What exactly counts as “timely”?

You must call "reportNewIncomingCall" before you return from the PushKit delegate handler.

Does CallKit need to be reported within the same run loop, or within a specific time window (for example, within 1 second)?

Strictly speaking, what's actually happening is that PKPushRegistry is checking a flag that CallKit sets (basically "did report call") as soon as you return from the delegate handler. If that flag isn't set, then it throws an exception, crashing your app.

More experienced developers might realize that this is not in fact a secure enforcement mechanism since, with enough work, it seems like it would be possible for an app to disrupt that mechanism. That is in fact true and, in fact, with enough digging, you can actually find the mechanism CallKit uses to notify PushKit of the call. I won't describe the details, but it isn't particularly difficult to "spoof" that mechanism. That's because the role of this particular crash isn't to formally enforce the requirement, but is instead to make it as obvious as possible what the problem actually is.

The ACTUAL enforcement mechanism is a second check in callservicesd that happens a few seconds (~7s) after the push is delivered to your app. Failing that check will then cause callservicesd to kill your app with the exception code "0xbaadca11".

The VAST majority of apps fail at the first check, which is easy to identify through the exception backtrace (which will show "PKPushRegistry") and this reason screen:

"Killing app because it never posted an incoming call to the system after receiving a PushKit VoIP push."

However, mistakes like failing to configure your PKPushRegistry delegate at launch or blocking too long in your PKPushRegistry delegate can cause the second crash.

Does the completion handler also have a similar timing requirement?

No. The completion handler has no role in any of this and probably shouldn't be in the API at all (at least not for VoIP). It was added many years when PushKit's role was expanded to other specialty pushes, but it never really "did" anything useful for VoIP pushes. It has a minor resource management role (so you should call it), but no role at all with any of this.

  1. Cold launch delay

When the app is not running, a VoIP push will cause the system to launch the app before calling didReceiveIncomingPushWithPayload. If the app launch process is slow, there may be a long delay between app launch and the push callback. Could this delay potentially cause the system to treat the push as improperly handled, resulting in a VoIP push restriction?

No, not really. The callservicesd check I mentioned above doesn't start until after the system has launched your app and callservicesd launch timeout is long enough that any app that fails that timeout is going to have much larger issues. Finally, keep in mind that your app controls when it sets up your PKPushRegistry delegate. If you're setting it up in applicationDidFinishLaunching (where we recommend), then the general system watchdog has tighter timing requirements than CallKit.

  1. Ending CallKit quickly when the app is in the foreground

When the app is already in the foreground, our current flow is: Receive VoIP push Report the call to CallKit After a very short delay (around 0.5 seconds), programmatically end the CallKit call Present the custom in-app call UI Could this behavior potentially be considered misuse of VoIP push or CallKit by the system?

So, the direct answer is "no". Particularly in the foreground, the system doesn't really "care" what your app does. In terms of the background, I'd have a minor concern around WHY you're doing this, as there is a very long-standing policy requirement that “VoIP" be used for "Calling Apps". That is, the "point" of the “VoIP" background category is to allow "people to call other people". Using it for any other roles is not allowed.

However, as long as the incoming call requests are "real", then I don't see any issue with what you're doing. Your app must report the call; it does not have to answer it.

One minor note on this point:

Present the custom in-app call UI

By design, CallKit doesn't present its own UI when your app is in the foreground specifically so you can present your own call UI.

Having said that, I do have two technical concerns:

First off, my major question would be why are you doing this? CallKit is an INCREDIBLE tool for VoIP apps, as it shifts their audio out of the normal audio priority system, solving a bunch of fairly weird and ugly edge cases. Frankly, saying "I'd like to make a VoIP app without CallKit" is the same as saying "I'd like to make a VoIP app that doesn't work very well". If you're making a VoIP app and don't think you need CallKit, then you're either doing something VERY strange or you haven't tested your app very well.

Secondly, in my experience, mixing CallKit and direct audio session management doesn't actually work that well. Direct session activation can prevent CallKit session activation from working properly, leading to weird edge case failures. My longstanding advice to all developers has been "use CallKit for all your audio" as mixing it with direct session control just doesn't really work all that well.

If your use case really is that different, then that's something I'd like to understand better, as I suspect you might be better off dropping PushKit entirely, relying on standard push instead. Keep in mind that standard high-priority alert pushes have the same delivery priority as VoIP pushes [1], so PushKit doesn't provide that much benefit without CallKit.

[1] Both types are handled the same by our pushes server, as you can't really do better than "deliver this right NOW".

  1. Recovery after VoIP push restriction

If the system determines that the app has violated VoIP push rules and VoIP pushes stop arriving, the only workaround we are currently aware of is: The user uninstalls and reinstalls the app. Are there any other ways to recover from this state while preserving user data?

No, not that the user really controls. I believe the mechanism also resets if/when the app is updated, but that isn't really something users can "do".

Also, if the user does nothing, will the system automatically remove the restriction after some time?

Yes, it should reset every ~24 hours. However, keep in mind that ALL of these issues should be treated as failures in your app you need to fix. There's no reason a properly implemented VoIP app should EVER crash for failing to report a call.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Questions about VoIP Push compliance rules and CallKit handling
 
 
Q