How to add CallKit to a Flutter app?

Add the flutter_callkit_incoming package, which wraps native CallKit (iOS) and ConnectionService (Android) behind one Dart API, and call its showCallkitIncoming method whenever a VoIP push or SIP INVITE arrives.

What is PushKit and why does a VoIP app need it?

PushKit delivers VoIP push notifications to iOS with high priority, waking the app even when fully terminated. Apple requires every PushKit push to result in a CallKit-reported incoming call within seconds, or future pushes are throttled.

Flutter VoIP App with CallKit & PushKit: Complete 2026 Guide

A working Flutter VoIP app needs native incoming-call handling, not just an in-app ringing screen. This guide covers PushKit, CallKit, and Android's equivalent — with runnable Dart code, since relatively few complete examples exist for Flutter specifically.

Beyond the core setup, we cover how this fits into a full SIP/WebRTC call flow, backend push delivery, edge cases the happy-path tutorials skip — a call arriving before the token registers, a user who's denied permissions, an already-active call — plus App Store review considerations, debugging techniques, and how to actually test this on real devices rather than trusting a simulator.

By Kaushik Parmar·16 min read·July 6, 2026

flutter_callkit_incoming

Core package

PushKit

iOS VoIP push

CallKit

iOS native call UI

ConnectionService

Android equivalent

The Incoming Call Sequence

SIP ServerVoIP PushFlutter AppCallKitINVITEPushKit payloadshowCallkitIncoming() — within seconds

Native Modules Behind the Flutter Package

It's worth understanding that flutter_callkit_incoming is a thin Dart wrapper around real native platform code — on iOS, it's calling Apple's CXProvider and CXCallController APIs directly; on Android, it's driving the platform's ConnectionService and telecom framework. This matters practically for two reasons: first, when something doesn't work as expected, the underlying native CallKit/ConnectionService documentation (not just the Flutter package's README) is often where the actual answer lives, since the plugin is a relatively thin bridge rather than a from-scratch reimplementation. Second, native iOS and Android engineers on your team can meaningfully review and debug this code even without deep Flutter expertise, since the concepts map directly onto native APIs they likely already know.

Why This Isn't Optional

iOS and Android both aggressively suspend backgrounded apps to save battery — a Flutter VoIP app that relies on a normal push notification or an in-app timer to detect an incoming call will simply miss calls once the app has been in the background for more than a few minutes. PushKit (iOS) and a high-priority FCM message with a foreground service (Android) are the only supported mechanisms for reliably waking the app for an incoming call, and CallKit/ConnectionService are required for the call to render as a real, native, full-screen incoming call rather than a notification banner.

How This Fits Into a Full SIP or WebRTC Call Flow

PushKit and CallKit solve the "how does the phone know to ring" problem — they don't carry any call media themselves. Once actionCallAccept fires, your app still needs to actually establish the call: register a SIP client (PJSIP, Linphone) or open a WebRTC/LiveKit connection, negotiate media, and start the audio path — all of that logic lives entirely outside CallKit's scope, triggered by the accept event as its starting signal. Teams new to this often assume CallKit "handles the call" once it's showing on screen; in reality it's purely the incoming-call UI and OS-level wake mechanism, with your app responsible for everything that makes the call actually connect and carry audio once the user taps accept.

Step 1: Add the Package

# pubspec.yaml
dependencies:
  flutter_callkit_incoming: ^2.x
  flutter_voip_push_notification: ^1.x  # PushKit wrapper

Step 2: Register for PushKit and Report to CallKit

import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart';
import 'package:flutter_callkit_incoming/entities/call_kit_params.dart';
import 'package:flutter_voip_push_notification/flutter_voip_push_notification.dart';

final voipPush = FlutterVoipPushNotification();

void setupPushKit() {
  voipPush.configure(onData: (Map<String, dynamic> payload) async {
    final params = CallKitParams(
      id: payload['call_id'],
      nameCaller: payload['caller_name'] ?? 'Unknown',
      handle: payload['caller_number'],
      type: 0, // 0 = audio, 1 = video
      duration: 30000,
      textAccept: 'Accept',
      textDecline: 'Decline',
    );
    // Must happen within seconds of receiving the push
    await FlutterCallkitIncoming.showCallkitIncoming(params);
  });

  voipPush.onTokenRefresh.listen((token) {
    // Send token to your backend so it can route pushes for this device
  });
}

The VoIP Services certificate used to send this push is generated separately from a normal APNs push certificate in the Apple Developer portal, under Certificates → VoIP Services.

Step 3: Handle Accept/Decline Events

FlutterCallkitIncoming.onEvent.listen((event) {
  switch (event!.event) {
    case Event.actionCallAccept:
      // Bring the app to foreground, start SIP/WebRTC call setup
      break;
    case Event.actionCallDecline:
      // Send a SIP CANCEL/BUSY or hang up the call leg
      break;
    case Event.actionCallEnded:
      // Clean up local call state
      break;
    default:
      break;
  }
});

Backend Push Delivery: What Your Server Needs to Do

This guide focuses on the Flutter app side, but the PushKit token your app registers is only useful once your backend can actually deliver a push to it when a call arrives. On an incoming SIP INVITE, your server-side call-signalling layer needs to look up the destination device's PushKit token, construct a properly formatted APNs VoIP push payload (distinct from a standard push notification payload), and send it via APNs' HTTP/2 API using your VoIP push certificate — with the notification's apns-push-type header explicitly set to voip, since a mismatched push type is a common, hard-to-diagnose reason pushes silently fail to trigger CallKit at all.

# Server-side push send (Python, using an APNs HTTP/2 client)
headers = {
    "apns-topic": "com.yourcompany.app.voip",
    "apns-push-type": "voip",
    "apns-priority": "10",
}
payload = {"call_id": call_id, "caller_name": caller_name, "caller_number": caller_number}
send_apns_push(device_token, payload, headers, cert=voip_push_cert)

Android: FCM High-Priority + ConnectionService

Android has no PushKit equivalent by name, but the same effect is achieved with a high-priority Firebase Cloud Messaging data message, which wakes a foreground service even when the app is killed. That service then calls the same flutter_callkit_incoming API — the package abstracts ConnectionService behind the identical Dart interface used for iOS, so the accept/decline event handling code above is shared across both platforms.

  • Use FCM data messages (not notification messages) with high priority so the payload reaches your app code directly
  • Declare a foreground service in AndroidManifest.xml with the phoneCall foreground service type (Android 14+ requirement)
  • Request READ_PHONE_STATE and, on Android 13+, POST_NOTIFICATIONS permissions at runtime

Handling Edge Cases

The happy-path flow — token registered, push arrives, CallKit shows the call — covers maybe 80% of real-world scenarios. The remaining 20% is where most shipped Flutter VoIP apps actually break in production:

  • A call arrives before the PushKit token has finished registering with your backend — this happens on first app launch if a call comes in during the registration round-trip; your backend needs to queue or reject calls to unregistered devices gracefully rather than crashing the push send
  • The user denies microphone or phone permissions — CallKit will still show the incoming call UI, but your app needs to detect the permission gap immediately on accept and show a clear in-call error rather than a silent, dead audio channel
  • A second call arrives while one is already active — decide explicitly whether your app supports call waiting (CallKit natively supports multiple simultaneous calls) or should reject/busy the second call, since the default behavior without explicit handling is undefined
  • The app was force-quit by the user (not just backgrounded) — PushKit still wakes it for VoIP pushes even in this state on iOS, but Android's equivalent depends on your foreground service being correctly declared; test this specific scenario explicitly, since it behaves differently from a normally backgrounded app

App Store Review Considerations

Apple's App Store review process specifically checks that PushKit's VoIP entitlement is used only for genuine incoming call notifications — apps that repurpose it as a general-purpose background wake-up mechanism are a common and well-documented rejection reason. Make sure your app's review notes clearly explain the VoIP use case, and — critically — that your test account can actually receive a real incoming call during review, since reviewers will attempt to trigger the exact flow this guide describes. Apps submitted without a working, testable incoming-call path are frequently rejected or delayed simply because the reviewer cannot verify the PushKit usage is legitimate.

Debugging Common Issues

  • Push arrives but CallKit never shows — almost always a missing or delayed showCallkitIncoming() call; add logging immediately on push receipt to confirm the handler is even firing before debugging further downstream
  • CallKit shows the call but audio never connects — check that your SIP/WebRTC call setup actually starts on the actionCallAccept event, not before; a common mistake is starting media setup on push receipt rather than on user acceptance
  • Works in TestFlight but not App Store review — almost always the VoIP push certificate environment mismatch (sandbox vs production APNs); confirm your backend is using the production APNs endpoint for release builds
  • Intermittent missed calls in production only — profile whether your backend's push-sending logic has retry/backoff on APNs failures, since a single failed push with no retry silently drops that call notification entirely

Testing on Real Devices

PushKit and CallKit cannot be fully tested on the iOS Simulator — VoIP push delivery requires a real device registered with a real APNs token. Budget for testing across at minimum: a call arriving while the app is in the foreground, backgrounded, and fully force-quit; a call arriving with the device locked; and a call arriving over both WiFi and cellular data, since push delivery timing can differ meaningfully between them. Android testing should equally cover Doze mode and battery-optimization settings, since some OEM Android skins (particularly on budget devices) apply aggressive background restrictions beyond stock Android's behavior, and can delay or drop your foreground service's wake-up entirely if the app isn't explicitly exempted by the user.

FAQ

Does Apple reject apps that use PushKit for non-VoIP purposes?

Yes — Apple's App Store review guidelines require PushKit VoIP entitlement to be used exclusively for actual VoIP call notifications. Using it to wake the app for other background tasks is a common rejection reason.

What happens if my app doesn't report to CallKit fast enough?

iOS silently throttles and eventually blocks further VoIP pushes to an app that doesn't consistently report received pushes to CallKit within a few seconds. This can look like 'calls randomly stop arriving' in testing if the reporting logic has any delay or failure path.

Can I customize the CallKit incoming call screen's appearance?

Partially — CallKit lets you set the caller name, an app icon/logo, and a ringtone, but the fundamental full-screen layout is controlled by iOS, not your app, by design.

Does flutter_callkit_incoming support call waiting?

Yes — CallKit natively supports multiple simultaneous calls; flutter_callkit_incoming exposes this through the same showCallkitIncoming API called again for a second call. Your app logic decides whether to allow it or respond busy.

Do I need a separate Android implementation if I'm only targeting iOS initially?

You still need to plan for it — flutter_callkit_incoming's Android path (ConnectionService via FCM) uses the same Dart API, so adding Android support later mostly means wiring up FCM registration rather than rewriting call-handling logic.

How long does Apple's PushKit throttling last if my app misbehaves?

Apple doesn't publish an exact duration, and it can escalate with repeated violations — the safest approach is simply never risking it: always call showCallkitIncoming synchronously within your push handler, with no conditional logic that could skip it.

Is flutter_callkit_incoming actively maintained?

As of this writing it remains the standard community package for this purpose in the Flutter ecosystem, with maintenance tracking new iOS and Android OS versions — check its changelog for compatibility with whatever Flutter and OS versions your app targets before adopting a new major version.

Should backend push delivery retry on failure?

Yes — a missed VoIP push means a missed call with no other fallback notification, so your backend should retry APNs/FCM delivery failures with backoff, and log delivery failures distinctly from successful sends so missed-call patterns are visible in your monitoring rather than silent.

Summary

The core PushKit-to-CallKit flow is a few hundred lines of well-understood Dart code — the real engineering effort in a production Flutter VoIP app goes into the edge cases (permissions denied, call waiting, force-quit state), backend push delivery correctness, and testing across real devices and real network conditions rather than a clean simulator run. Get the happy path working first, then work through this guide's edge-case list methodically before shipping to real users.

Building a Flutter VoIP App?

CelloIP builds production Flutter VoIP apps with full PushKit/CallKit integration on both platforms.