CallKit is Apple's framework that gives a VoIP app the same native calling experience as the Phone app: lock-screen incoming calls, system call UI, and audio session priority. Mastering CallKit means wiring PushKit VoIP pushes, a CXProvider, a CXCallController, and AVAudioSession coordination together correctly. If you build with a managed RTC platform like VideoSDK, CallKit sits on top of your media layer, not in place of it. This guide walks through the full integration, from project setup to App Store-ready testing.
A VoIP app that shows a custom in-app banner for an incoming call feels broken on iOS, and Apple's review guidelines treat it that way. Since iOS 13, every app that receives VoIP pushes must report the call to CallKit before the push completion handler returns, or the system terminates the app and eventually stops delivering pushes at all. That single requirement reshapes how a modern iOS calling app is architected.
By the end of this guide you will understand the CallKit component model, how to configure your Xcode project correctly, how to implement both incoming and outgoing call flows, how to coordinate audio sessions with the system, and how to debug the failures that almost every team hits on the way to shipping.

Understanding the CallKit Architecture

CallKit is a coordination layer, not a media engine. It tells iOS that your app is participating in a phone call, and in exchange iOS gives your app the native incoming-call interface, lock screen and Dynamic Island integration, Do Not Disturb and Focus compliance, and elevated audio session priority. Your app still owns the actual voice transport, whether that is SIP signaling over UDP, a WebRTC peer connection, or a managed SDK.
The framework revolves around a handful of classes that split responsibilities cleanly. The provider is the bridge to the system, the call controller is the bridge to your app's own UI and logic, and transactions wrap every state change so the system and your app stay in agreement about what happened to a call.

Core Classes and Their Roles

The CXProvider represents your app to the system call UI. It receives user actions (answer, end, mute) and lets you report call state changes outward. The CXCallController lets your app request call actions and manages the list of active calls. A CXTransaction groups one or more actions so they are applied atomically. A CXAction is the base unit of change, such as answering or ending a call. The CXCallUpdate carries call metadata, including the caller's handle, localized name, and whether video is supported. The CXHandle identifies the remote party, typically as a phone number or email address.

How CallKit Fits with PushKit and AVAudioSession

The three frameworks form a pipeline. PushKit wakes your app in the background when a VoIP push arrives. Your app then reports the incoming call to CallKit through the provider. Once the user answers, CallKit activates the audio session for you, and your VoIP engine (SIP stack or WebRTC layer) starts or resumes media flow over that session. The diagram later in this article shows this flow end to end.

Setting Up the Project for VoIP

Project configuration is where most CallKit integrations quietly fail. The system is strict about capabilities, and a missing entitlement produces silent push delivery failures rather than readable errors.
Start in Xcode by adding the Push Notifications capability and the Background Modes capability to your app target. Under Background Modes, enable the Voice over IP option (and Audio if you play ringtone or hold audio in the background). Then create an App ID in the Apple Developer portal with Push Notifications enabled, create a VoIP push certificate (not a standard APNs certificate) for that App ID, and download and install it. Your push server must use this certificate when sending VoIP pushes through APNs.
In Info.plist, add the NSMicrophoneUsageDescription privacy string with a clear human-readable explanation, because the microphone permission prompt appears during your first call. If you support Call Directory features, you will also add a Call Directory extension target, which is a separate process with its own identifier.

Enable Background Modes and Push Notifications

The exact capabilities to enable are: Push Notifications, Background Modes with Voice over IP checked, and Audio, AirPlay, and Picture in Picture if your app plays audio while backgrounded. Without the Voice over IP background mode, iOS will not deliver VoIP pushes to your app when it is suspended, which is precisely when most calls arrive.

Configure the Call Directory Extension (Optional)

Add a Call Directory extension only if you need system-level caller ID identification or call blocking. The extension receives your app's contact and block lists and exposes them to the system Phone app, so blocked numbers are silenced even before your app launches. If your app only shows caller identity inside its own UI, skip the extension entirely; it adds a target, a review surface, and maintenance burden.

Provider Configuration: Building a Reliable CXProvider

The provider is created once, typically as a singleton, and lives for the app's entire lifetime. Its configuration object declares what your app supports: which handle types (phone numbers, email addresses), the localized name shown in the system call UI, whether video calls are supported, and the maximum number of calls the user can hold simultaneously.
Choose handle types that match how users are identified in your system. A consumer calling app keyed on phone numbers should register phone number handles so the system can match against contacts and the Call Directory. A business or social calling app keyed on email or usernames should register the corresponding handle type, accepting that system-level contact matching will be weaker.
Set the maximum calls count realistically. Supporting two concurrent calls means your UI, your audio routing, and your VoIP engine all need to handle two active sessions. Most apps ship with a limit of one or two.

Best-Practice Settings for Production

Three settings deserve attention before shipping. First, decide whether calls appear in the system Recents list; including them improves user trust and enables callbacks from the Phone app, but it means your app must handle being launched directly into a call. Second, you can supply a custom ringtone for incoming calls, which matters for apps where users must distinguish personal and business calls. Third, keep the localized provider name short, because it appears in the system call UI where long names truncate.

Implementing the Incoming Call Flow

The incoming call flow is the part Apple polices hardest. When a VoIP push arrives, PushKit wakes your app and hands the payload to your push registry delegate. At that moment you have a hard obligation: create a CXCallUpdate with the caller's handle and localized name, and report the new incoming call to the provider before you call the push completion handler.
The timing constraint is the crux. If you report the call late, or not at all, iOS treats the push as mishandled. The first offense logs an error; repeated offenses cause the system to terminate your app on push receipt and eventually stop delivering VoIP pushes to your device entirely. Teams hit this constantly during development because they test with the app foregrounded, where the enforcement is more forgiving.
The sequence in practice: push arrives, your delegate extracts the caller identity and call identifier from the payload, you build the call update, you report the incoming call, and only then do you signal push completion. If your server payload lacks caller metadata, fix the server, because you cannot fetch it asynchronously before reporting.

PushKit Registration and Handling

Your app registers with the VoIP push service at launch and receives a device token, which you upload to your push server. The push registry delegate then has two jobs: report token changes, and handle incoming VoIP payloads by immediately reporting the call to CallKit. Every payload your server sends must correspond to a real incoming call, because Apple rejects apps that use VoIP pushes for anything else, such as chat notifications.

Reporting New Incoming Call

The CXCallUpdate you provide must include a CXHandle for the remote party, a localized caller name for the system UI, and a flag for video support if relevant. The call identifier you pass must be a stable unique value, because every subsequent action on that call (answer, end, hold) references it. Report the call, then complete the push. When the user answers, the provider delegate's answer action fires, and that is where you start media and configure audio.

Outgoing Call Flow and User-Initiated Actions

Outgoing calls flow in the opposite direction. Your app's UI, say a contact card or a dial pad, triggers a call request through the CXCallController. You build a CXCallAction to start the call with the destination handle, wrap it in a transaction, and request the transaction. The system then presents the native outgoing call UI, and your provider delegate receives the start-call action callback, which is your signal to begin signaling and media negotiation with your VoIP engine.
The transaction model keeps both sides honest. When the user taps end while your engine is still negotiating, the end action arrives and you must tear down gracefully. When your engine fails to connect, you report the call as ended with a failure reason, and the system UI reflects it. Treat every action callback as a request you must fulfill and then confirm, not a notification.

Managing Call Actions (Answer, End, Hold, Mute)

Map each provider action to your VoIP engine explicitly. Answer means activate media and start audio. End means tear down the session and release the audio session. Hold means pause media without ending the call, typically muting both directions. Mute means silence your outgoing track only. Each action must be fulfilled and confirmed, or the system call UI will hang in an inconsistent state, which is one of the most visible bugs a calling app can ship.

Audio Session Coordination

CallKit changes how AVAudioSession works, and misunderstanding this is the most common source of audio bugs. When your app participates in a CallKit call, the system owns audio session activation. You configure the session's category and mode (play-and-record with voice-chat mode is standard for calls), but you do not activate it yourself; CallKit activates it when the call is answered and deactivates it when the call ends.
If your code manually activates or deactivates the session during a CallKit call, you will fight the system and produce symptoms like one-way audio, dropped audio after a hold, or audio that dies when the app is backgrounded. Configure the session early, before the first call, and let CallKit drive activation for the lifetime of every call.

Handling Interruptions and Route Changes

Observe audio session interruptions and route changes. When a user switches from Bluetooth to speaker, or unplugs headphones mid-call, a route change notification fires and your engine should reattach media to the new route. Interruptions, such as a FaceTime call arriving or Do Not Disturb behavior, require you to pause cleanly and resume when the interruption ends. Test Bluetooth, AirPlay, and wired headphones explicitly, because route handling is where WebRTC and SIP stacks most often diverge from expected behavior.

Call Directory Extension: Caller ID and Blocking

The Call Directory extension runs as a separate process that the system queries when a call arrives, before your app is involved. You provide two kinds of entries: identification entries that map phone numbers to names your app knows, and blocking entries that tell the system to silence calls from specific numbers.
The extension reloads when the system asks it to, and your app requests a reload whenever its contact or block lists change. Keep the lists bounded; the extension has a memory budget, and oversized lists cause silent reload failures. For a spam-filtering or business-caller-ID app this extension is essential; for a simple peer-to-peer calling app it is usually unnecessary complexity.

Common Pitfalls and Debugging Tips

Four failures account for most CallKit bug reports. First, reporting the incoming call after the push completion handler returns, which triggers the system's enforcement and eventual push termination. Second, a missing or wrong VoIP push certificate on the server, which produces pushes that never arrive and no client-side error to inspect. Third, a misconfigured background mode, where Voice over IP is unchecked and pushes only arrive while the app is foregrounded. Fourth, unfulfilled or failed transactions, where an action callback is never confirmed and the system call UI freezes on "connecting."
Diagnose systematically. If pushes do not arrive, verify the certificate and the token registration server-side before touching CallKit code. If pushes arrive but the system UI never shows, check the report timing against the push completion handler. If the UI shows but audio fails, audit every manual audio session activation in your codebase.

Using Xcode Logs and Console Filters

On a real device, open Console and filter by your app's process name plus the CallKit subsystem. The system logs report-call timing violations, transaction failures, and audio session ownership disputes with surprising clarity once you know the subsystem names to filter on. Reproduce issues with the app cold-launched and backgrounded, because foreground testing hides the strictest enforcement paths.

Architecture Diagram

The full pipeline from an incoming push to live audio looks like this:

Production Checklist

Before submitting to the App Store, verify every item on this list:
  • Push Notifications capability and VoIP push certificate installed and used by your push server
  • Background Modes with Voice over IP enabled
  • NSMicrophoneUsageDescription present with a clear explanation
  • Every VoIP push payload contains caller handle, name, and a unique call identifier
  • Incoming call reported to CallKit before the push completion handler returns
  • All provider actions (answer, end, hold, mute) fulfilled and confirmed
  • No manual AVAudioSession activation or deactivation during CallKit calls
  • Audio route changes and interruptions handled and tested
  • Multi-call behavior tested if maximum calls is greater than one
  • Call Directory extension lists bounded and reload tested, if used
  • Incoming and outgoing calls tested on real devices with the app cold-launched and backgrounded

Testing Strategy on Real Devices

CallKit cannot be meaningfully tested in the Simulator: VoIP pushes, lock screen calls, Bluetooth routing, and the Call Directory extension all require real hardware. Build a minimal push test harness on your server so you can fire a VoIP push at a registered device on demand.
Test the states users actually hit: app cold-launched and screen locked, app backgrounded for hours, app mid-call when a second call arrives, and a call interrupted by a native phone call. For App Store review, expect the reviewer to verify that incoming calls show the system UI, that the app does not use VoIP pushes for non-call purposes, and that the privacy usage description matches actual behavior. Keep a demo account with a callable counterpart ready in your review notes.

Final Thoughts and Next Steps

CallKit rewards apps that respect its contract: report calls on time, fulfill every action, and let the system own the audio session. Most production failures trace back to breaking one of those three rules rather than to exotic bugs.
If you are building the media layer under CallKit, a managed RTC platform removes the signaling, TURN infrastructure, and audio pipeline work that CallKit does not cover. VideoSDK's iOS SDK handles rooms, participants, and audio transport, and its REST APIs let your server create rooms and manage calls while CallKit owns the native UI. Apple's CallKit documentation and the VideoSDK code samples are the best next stops.

Definitions Glossary

CXProvider: The CallKit object that represents your app to the system call UI, receiving user actions like answer and end, and letting your app report call state changes outward.
CXCallController: The CallKit object your app uses to request call actions, such as starting an outgoing call, and to track the list of active calls.
CXTransaction: A wrapper that groups one or more call actions so the system and your app apply them atomically and stay in agreement about call state.
CXCallUpdate: The metadata object describing an incoming call, including the remote party's handle, localized caller name, and video support flag.
PushKit VoIP push: A special high-priority APNs notification that wakes an iOS app in the background for an incoming call, requiring the app to report the call to CallKit immediately.
Call Directory extension: A separate app process that supplies caller identification and call blocking entries to the system Phone app before your app launches.

Key Takeaways

  • iOS requires every VoIP push to be reported to CallKit as an incoming call before the push completion handler returns, or the system will terminate the app and eventually stop delivering pushes.
  • CallKit is a coordination layer, not a media engine; your VoIP transport, whether SIP, WebRTC, or a managed SDK like VideoSDK, still carries the actual audio.
  • During a CallKit call the system owns audio session activation, so manual AVAudioSession activation is the root cause of most one-way and dropped-audio bugs.
  • Every provider action (answer, end, hold, mute) must be fulfilled and confirmed, or the native call UI freezes in an inconsistent state.
  • Real-device testing with the app cold-launched and backgrounded is mandatory, because the Simulator cannot exercise VoIP pushes, lock screen calls, or audio routing.

Conclusion

Mastering CallKit for VoIP integration on iOS comes down to respecting three contracts: report incoming calls on time, fulfill every action callback, and let the system own the audio session. Get those right and the native calling experience, from lock screen to Dynamic Island, works in your favor. For the media layer underneath, VideoSDK's iOS SDK and its free tier let you stand up rooms, participants, and audio transport quickly while CallKit handles the native UI. What are you building with CallKit and VideoSDK? Drop a comment, I'd love to hear what kind of calling app you're working on.

Free $20 Balance for AI Voice Agents & Video Calls

FAQ