A Flutter plugin that integrates the device's native call infrastructure into VoIP and WebRTC applications. Instead of building a custom call UI from scratch, the app delegates call presentation and control to the OS — the same experience users get with regular phone calls.
iOS uses CallKit and PushKit. The system lock-screen incoming-call UI appears even when the app is terminated. PushKit wakes the app on an incoming VoIP push before the user sees anything, giving the app time to establish media before accepting.
Android uses the Telecom ConnectionService API running in a dedicated :callkeep_core
process. This process stays alive independently of the main app, so calls survive app
backgrounding and process death. Incoming calls are presented via Flutter UI backed by Android
foreground services, and outgoing calls integrate with the system dialer, Bluetooth headsets, and
audio routing.
Both platforms expose a unified Dart API: report calls, control audio, and receive delegate callbacks — without writing platform-specific code in your app.
This is a federated plugin. Each package has its own README and docs/ directory.
| Package | Description |
|---|---|
webtrit_callkeep |
Public API — aggregates the platform implementations |
webtrit_callkeep_platform_interface |
Shared Dart interface and models |
webtrit_callkeep_android |
Android implementation (Telecom + foreground services) |
webtrit_callkeep_ios |
iOS implementation (CallKit + PushKit) |
| Platform | Minimum version |
|---|---|
| Android | API 26 (Android 8.0) |
| iOS | iOS 11 |
dependencies:
webtrit_callkeep: ^<version>Call setUp once after your app is ready (main isolate only).
await callkeep.setUp(
CallkeepOptions(
ios: CallkeepIOSOptions(
localizedName: 'My App',
ringtoneSound: 'assets/ringtones/incoming.caf',
iconTemplateImageAssetName: 'assets/callkeep_icon.png',
maximumCallGroups: 1,
maximumCallsPerCallGroup: 1,
supportedHandleTypes: {CallkeepHandleType.number},
),
android: CallkeepAndroidOptions(
ringtoneSound: 'assets/ringtones/incoming.mp3',
ringbackSound: 'assets/ringtones/outgoing.mp3',
),
),
);callkeep.setDelegate(MyCallkeepDelegate());
// iOS only — handle PushKit VoIP tokens and push payloads
callkeep.setPushRegistryDelegate(MyPushRegistryDelegate());await callkeep.reportNewIncomingCall(
callId,
CallkeepHandle.number('+15551234567'),
displayName: 'John Doe',
hasVideo: false,
);await callkeep.startCall(
callId,
CallkeepHandle.number('+15559876543'),
displayNameOrContactIdentifier: 'Jane Doe',
hasVideo: false,
);await callkeep.tearDown();Use these methods to notify the platform about call state changes.
| Method | Description |
|---|---|
setUp(options) |
Register phone account, initialize notification channels |
tearDown() |
Hang up all calls and release resources |
reportNewIncomingCall(callId, handle, ...) |
Register an incoming call with the platform |
reportConnectingOutgoingCall(callId, handle, ...) |
Mark outgoing call as connecting |
reportConnectedOutgoingCall(callId, handle, ...) |
Mark outgoing call as connected |
reportUpdateCall(callId, ...) |
Update call metadata (display name, video, etc.) |
reportEndCall(callId, reason) |
Notify platform the call ended |
startCall(callId, handle, ...) |
Initiate an outgoing call |
answerCall(callId) |
Answer a call programmatically |
endCall(callId) |
End a call |
setHeld(callId, onHold) |
Put call on hold or resume |
setMuted(callId, muted) |
Mute or unmute |
setSpeaker(callId, on) |
Toggle speaker |
sendDTMF(callId, digit) |
Send a DTMF tone |
Implement CallkeepDelegate and pass it to setDelegate to receive platform events.
| Method | When it fires |
|---|---|
didPushIncomingCall(callId, handle, ..., error) |
Platform has registered the incoming call (or reports an error) |
performAnswerCall(callId) |
User answered from system UI |
performEndCall(callId) |
User ended from system UI or system terminated the call |
performStartCall(callId, handle, ...) |
User initiated outgoing call from system UI (e.g. Siri) |
continueStartCallIntent(callId, handle, ...) |
System confirmed outgoing call intent |
performSetHeld(callId, onHold) |
User toggled hold from system UI |
performSetMuted(callId, muted) |
User toggled mute from system UI |
performSendDTMF(callId, digit) |
User sent DTMF from system dial pad |
performSetSpeaker(callId, on) |
User toggled speaker from system UI |
didActivateAudioSession() |
System activated the audio session |
didDeactivateAudioSession() |
System deactivated the audio session |
didReset() |
System reset all call state |
perform* methods return Future<bool>. Return false to signal failure — the platform will
terminate the call.
Android requires a running service to handle calls when the app is backgrounded. The plugin provides a push notification isolate — a short-lived Flutter isolate spawned when an FCM (or other) push arrives. It exits after the call ends.
// Register the isolate entry-point once (main isolate, before background activity):
await AndroidCallkeepServices.backgroundPushNotificationBootstrapService
.initializeCallback(onPushNotificationCallback);
// From your FCM background handler:
@pragma('vm:entry-point')
Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await AndroidCallkeepServices.backgroundPushNotificationBootstrapService
.reportNewIncomingCall(
callId,
CallkeepHandle.number(handle),
displayName: displayName,
hasVideo: hasVideo,
);
}
// The isolate callback:
@pragma('vm:entry-point')
Future<void> onPushNotificationCallback(
CallkeepPushNotificationSyncStatus status,
CallkeepIncomingCallMetadata? metadata,
) async {
await initializeDependencies();
switch (status) {
case CallkeepPushNotificationSyncStatus.synchronizeCallStatus:
await backgroundCallManager.onStart();
case CallkeepPushNotificationSyncStatus.releaseResources:
await backgroundCallManager.close();
}
}Inside the isolate, use CallkeepBackgroundServiceDelegate to receive answer/end events and
BackgroundPushNotificationService to report call outcomes back to the platform.
Earlier versions also included a persistent signaling isolate — a long-lived foreground service that kept a Flutter isolate running with an open WebSocket connection to the signaling server. It was removed because Android increasingly restricts persistent background services (battery optimization, Doze mode, vendor-specific kill policies), making reliable long-running isolates impractical. Signaling is also application-level responsibility: the plugin is concerned with presenting calls to the OS, not maintaining a connection. FCM high-priority push is the recommended and sufficient mechanism to wake the device for an incoming call.
Android only. Allows triggering an incoming call from a specially formatted SMS — useful when push delivery is unreliable.
Required permissions: RECEIVE_SMS, BROADCAST_SMS
The SMS must start with the prefix <#> WEBTRIT: (hard-coded security filter, do not change).
The rest of the message is parsed by a caller-supplied ICU-compatible regex that captures 4 groups
in order: callId, handle, displayName, hasVideo.
await callkeep.initializeSmsReception(
messagePrefix: '<#> WEBTRIT:',
regexPattern: r'your-regex-here',
);Full regex specification: docs/sms_trigger_regex_requirements.md
SMS access is a sensitive Play Store permission. You must justify its use and ensure the regex only matches messages from your own backend.
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
<!-- Optional: SMS fallback -->
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.BROADCAST_SMS" />- Enable Push Notifications capability
- Enable Background Modes -> Voice over IP
The public API is covered by integration tests in
webtrit_callkeep/example/integration_test/.
Tests cover: lifecycle, incoming/outgoing call scenarios, state machine (hold, mute, DTMF), foreground service timing, push notification background service path, connection queries, delegate edge cases, and stress/concurrency scenarios.
cd webtrit_callkeep/example
flutter test integration_test/<test_file>.dartwebtrit_phone is a reference Flutter VoIP app that
demonstrates real-world usage of this plugin including signaling, media, background call handling,
and full foreground/background workflows.
See CONTRIBUTING.md for branch naming, commit message format, and PR conventions.