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:
-
Is already on a call.
-
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:
-
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.
-
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