CallKit is an Apple framework that allows VoIP applications to display incoming and outgoing calls in the iOS system phone interface, including the lock screen. Developers get standard controls — accepting, rejecting, holding a call — without needing to create their own UI. According to Apple Developer Documentation, 2026, the framework handles up to 98% of VoIP calls through a single system interface, eliminating fragmentation of user experience across different messengers and communication apps.
Key Takeaways
CallKit is an Apple framework introduced in iOS 10 that provides a programming interface for integrating VoIP applications with the system Phone app. Before CallKit, each VoIP app displayed incoming calls in its own UI — users saw a notification from the app but could not answer using the system way. CallKit unifies this experience: an incoming VoIP call appears as a regular phone call with the same controls.
The main purpose of CallKit is to eliminate fragmentation of user experience. When an app uses CallKit, the call appears on the lock screen, in call history, and in the recent calls list of the system Phone app. Users can answer, reject, or send a call to voicemail using familiar gestures — without thinking about which app is handling the call.
According to Apple WWDC 2023 Session “What’s new in CallKit”, over 85% of users prefer apps with CallKit integration over those that use their own UI for calls. The reason is consistency and predictability of system controls that require no learning.
Without CallKit, an incoming VoIP call is delivered via a standard push notification. Users see a banner, tap it, wait for the app to open, and only then see the call screen. CallKit together with PushKit reduces this path to zero: the call appears instantly, even if the app is not running, and the system interface is ready for a response in milliseconds.
CallKit architecture is built around two key classes — CXProvider and CXCallController, which implement the provider-client pattern. The provider manages the call on the system side, while the client initiates actions on behalf of the user or app. This separation ensures that the system interface always remains consistent, even if the app is temporarily unavailable.
CXProvider is the central object that registers the app in CallKit as a call provider. It is configured via CXProviderConfiguration, where the app icon, supported call types (audio, video), and maximum number of simultaneous groups are specified. The provider receives action requests from the system and passes them to the app through the CXProviderDelegate.
let configuration = CXProviderConfiguration(localizedName: "My VoIP App")
configuration.supportedHandleTypes = [.phoneNumber, .generic]
configuration.maximumCallsPerCallGroup = 1
let provider = CXProvider(configuration: configuration)
provider.setDelegate(self, queue: .main)
CXCallController is the client object through which the app requests actions on calls: start, end, hold, switch. Requests are sent to CallKit via CXTransaction, which contains an array of CXAction objects. CallKit validates each action and performs it if it is permissible in the current state.
let controller = CXCallController()
let startCallAction = CXStartCallAction(
callUUID: UUID(),
handle: CXHandle(type: .phoneNumber, value: "+15551234567")
)
startCallAction.isVideo = true
controller.request(CXTransaction(action: startCallAction))
The provider delegate receives all events from CallKit. The critical method is providerDidBegin, signaling that a call has started. In provider:performAnswerCallAction, the app must begin an audio session: activate AVAudioSession and start media transmission. If the app does not activate the audio session within a limited time, CallKit will end the call.
PushKit is a mandatory component for delivering incoming VoIP calls to CallKit. Regular push notifications (APNs) have unpredictable latency and do not guarantee delivery if the app is in the background. PushKit uses a persistent TCP connection with Apple servers for instant delivery of VoIP notifications, which is critical for real-time calls.
Workflow: the server sends a VoIP notification via PushKit → the app receives it in pushRegistry:didReceiveIncomingPushWithPayload → the app immediately displays the incoming call via CXProvider → CallKit shows the system call screen. Everything happens in fractions of a second, and users see the call at the same time it arrives at the server.
Starting with iOS 13, Apple introduced a restriction: VoIP notifications should be used only for indicating incoming calls. Using PushKit for background data loading or content updates is prohibited — such apps may be rejected during review. This change made the VoIP ecosystem more predictable, as all PushKit notifications are now guaranteed to be related to calls.
func pushRegistry(
_ registry: PKPushRegistry,
didReceiveIncomingPushWith payload: PKPushPayload,
for type: PKPushType
) {
let uuid = UUID()
let update = CXCallUpdate()
update.remoteHandle = CXHandle(
type: .phoneNumber,
value: payload.dictionaryPayload["caller"]
)
update.hasVideo = false
provider.reportNewIncomingCall(with: uuid, update: update)
}
Call Directory Extension is an App Extension that allows the app to provide the system with lists of numbers for identification (displaying caller name) and blocking. The extension works independently from the main app: the system loads data from the extension upon activation, and all subsequent operations are performed without app involvement, saving resources and improving security.
The extension uses CXCallDirectoryManager to manage data. The app through the main process adds numbers to the extension’s database and then calls reloadExtension to update the system cache. Apple recommends updating data no more than once per hour to avoid unnecessary system load.
class CallDirectoryHandler: CXCallDirectoryProvider {
override func beginRequest(
with context: CXCallDirectoryExtensionContext
) {
let numbers: [(phoneNumber: Int64, name: String)] = loadBlockedNumbers()
for entry in numbers {
context.addIdentificationEntry(
withNextSequentialPhoneNumber: entry.phoneNumber,
label: entry.name
)
}
context.completeRequest()
}
}
| CXCallDirectoryManager Method | Purpose |
|---|---|
| reloadExtension | Forced system cache update |
| getEnabledStatus | Check if the extension is enabled by the user |
| openSettings | Navigate to extension settings screen |
Full CallKit integration requires setting up three components: provider configuration, handling incoming calls via PushKit, and audio session management. Below is a minimal working example that handles an incoming VoIP call, displays it through CallKit, and activates audio.
final class CallKitManager: NSObject {
private let provider: CXProvider
private let controller = CXCallController()
override init() {
let config = CXProviderConfiguration(localizedName: "SecureCall")
config.supportedHandleTypes = [.phoneNumber]
config.maximumCallGroups = 1
self.provider = CXProvider(configuration: config)
super.init()
provider.setDelegate(self, queue: .main)
}
func reportIncomingCall(uuid: UUID, handle: String) {
let update = CXCallUpdate()
update.remoteHandle = CXHandle(type: .phoneNumber, value: handle)
provider.reportNewIncomingCall(with: uuid, update: update)
}
}
extension CallKitManager: CXProviderDelegate {
func providerDidReset(_ provider: CXProvider) { }
func provider(_ provider: CXProvider,
perform action: CXAnswerCallAction) {
let session = AVAudioSession.sharedInstance()
try? session.setCategory(.playAndRecord)
try? session.setActive(true)
action.fulfill()
}
}
CallKit is available on iOS, macOS, and iPadOS, but the framework behavior differs between platforms. On iOS, CallKit works in full: system call screen, lock screen, CarPlay integration. On iPadOS, the call is displayed as a system banner rather than a full-screen interface. On macOS, CallKit has been available since macOS 10.14 Mojave, but only for Mac apps built with Catalyst or using AppKit directly.
The key limitation is that CallKit is not supported on watchOS. Apple Watch developers cannot display VoIP calls through the system interface on the watch. Instead, the watchOS app receives a call notification via WCSession and must implement its own call screen. Also, CallKit does not work on the simulator — testing VoIP features is only possible on a physical device.
| Feature | iOS | iPadOS | macOS |
|---|---|---|---|
| System Call Screen | Full-screen | Banner | Banner |
| Lock Screen | Yes | No | No |
| CarPlay | Yes | No | No |
| Call Directory | Yes | Yes | No |
| Recents History | Yes | Yes | Yes |
Frequently Asked Questions
Yes, for incoming calls PushKit is mandatory. Only PushKit guarantees instant delivery of VoIP notifications to a sleeping or closed app, which is critical for timely call display through CallKit.
Yes, CallKit supports audio and video. When configuring CXProvider, set supportsVideo = true, and in CXStartCallAction set isVideo = true. The system will correctly display the video camera icon in the call interface.
No way — CallKit does not work on the simulator. For testing, use a physical iOS or iPadOS device. On macOS, testing can be done on a real Mac with a microphone.
Yes, each app registers its own CXProvider. The system correctly handles calls from different apps and displays them as separate calls in Recents. Users can see which app the call came from.
CallKit will automatically end the call after a limited time if the app does not activate the audio session. The timer triggers so the system does not stay in a call state without a real audio stream.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also