incoming_call_kit
A highly customizable Flutter plugin for incoming, outgoing, and missed call UI.
Native full-screen Activity on Android. Apple CallKit on iOS.
Works with Twilio, Agora, WebRTC, Firebase, Vonage, Stream, or any VoIP backend.
Screenshots
Full-screen call UI Incoming call notification
Table of Contents
- Features
- How It Works
- Installation
- Android Setup
- iOS Setup
- Quick Start
- Customization
- VoIP Integration Guides
- Event Reference
- API Reference
- Architecture
- Troubleshooting
- Support
- Connect
β¨ Features
| Feature | Android | iOS |
|---|---|---|
| Incoming call full-screen UI | Custom native Activity | CallKit |
| Outgoing call management | Notification + timer | CallKit |
| Missed call notification | System notification | UNNotification |
| Lock screen / background | showWhenLocked + FGS |
CallKit (native) |
| Gradient backgrounds | Linear & Radial | N/A (CallKit) |
| Avatar with pulse animation | Native Activity | N/A (CallKit) |
| Swipe-to-answer gesture | Native Activity | N/A (CallKit) |
| Custom ringtone & vibration | Per-call, ringer-aware | Per-provider |
| Background event handler | Headless FlutterEngine | Headless FlutterEngine |
| PushKit / VoIP token | N/A | PKPushRegistry |
| OEM autostart detection | 7 manufacturers | N/A |
CallStyle notification |
API 31+ native treatment | N/A |
| Multi-call support | Per-call notification IDs | Per-UUID tracking |
| Pending event replay | SharedPreferences | In-memory queue |
| Foreground service fallback | Graceful on Android 12+ | N/A |
| Android 15 compliance | FGS subtype declared | N/A |
π± How It Works
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Your Flutter App β
β β
β VoIP Push (Twilio / Agora / FCM / PushKit) β
β β β
β βΌ β
β IncomingCallKit.instance.show(params) β
β β β
β βββββββ΄βββββββ β
β βΌ βΌ β
β Android iOS β
β β β β
β Foreground CXProvider β
β Service .reportNewIncomingCall() β
β β β β
β βΌ βΌ β
β Notification Native CallKit UI β
β + Full-screen (Apple-designed) β
β Activity β β
β (lock/bg) β β
β β β β
β βΌ βΌ β
β User taps User taps β
β Accept/Decline Accept/Decline β
β β β β
β βΌ βΌ β
β EventBus βββΊ Dart onEvent stream βββ CXProviderDelegate β
β β β
β βΌ β
β Connect your Twilio / Agora / WebRTC call β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Key idea: This plugin handles the call UI only β the ringing screen, notifications, and user actions. Your VoIP SDK (Twilio, Agora, WebRTC, etc.) handles the actual audio/video connection. They work together like this:
- VoIP push arrives β you call
callKit.show()to display the call screen - User taps Accept β you receive
CallKitAction.acceptβ you connect your VoIP SDK - User taps Decline β you receive
CallKitAction.declineβ you reject on your server - Remote side hangs up β you call
callKit.dismiss()β call screen disappears
π¦ Installation
Add to your pubspec.yaml:
dependencies:
incoming_call_kit: ^0.0.1
Then run:
flutter pub get
Requirements:
| Minimum | |
|---|---|
| Flutter | 3.27.0 |
| Dart SDK | 3.6.0 |
Android minSdk |
24 (Android 7.0) |
Android compileSdk |
36 |
| iOS | 13.0 |
π€ Android Setup
1. Set minimum SDK
In your app's android/app/build.gradle:
android {
defaultConfig {
minSdk = 24 // Required: Android 7.0+
}
}
2. Permissions
All permissions are declared by the plugin automatically:
| Permission | Purpose |
|---|---|
FOREGROUND_SERVICE |
Keep call alive in background |
FOREGROUND_SERVICE_PHONE_CALL |
Phone call FGS type |
POST_NOTIFICATIONS |
Show call notification (Android 13+) |
USE_FULL_SCREEN_INTENT |
Lock screen display (Android 14+) |
WAKE_LOCK |
Wake device on incoming call |
VIBRATE |
Vibration during ring |
INTERNET |
Load avatar images |
No
SYSTEM_ALERT_WINDOWneeded. The plugin usesUSE_FULL_SCREEN_INTENT+ foreground service instead.
3. Runtime permissions
Request at runtime using the built-in helpers β see Permissions.
4. OEM autostart (Xiaomi, OPPO, Vivo, etc.)
Some OEMs kill background services. Guide users to whitelist your app β see OEM Autostart.
π iOS Setup
1. Enable background modes
In Xcode: Runner β Signing & Capabilities β + Capability β Background Modes:
xVoice over IPxRemote notifications
Or in ios/Runner/Info.plist:
<key>UIBackgroundModes</key>
<array>
<string>voip</string>
<string>remote-notification</string>
</array>
2. Enable Push Notifications
In Xcode: Signing & Capabilities β + Capability β Push Notifications.
3. CallKit icon (optional)
Add a 40Γ40pt single-color PNG named CallKitLogo to your asset catalog. This appears in the native CallKit UI.
4. Custom ringtone (optional)
Add a .caf or .aiff file to your Xcode project:
ios: IOSCallKitParams(
ringtonePath: 'MyRingtone.caf',
),
π Quick Start
1. Show an Incoming Call
import 'package:incoming_call_kit/incoming_call_kit.dart';
final callKit = IncomingCallKit.instance;
await callKit.show(
CallKitParams(
id: 'call-123', // Unique call ID from your server
callerName: 'John Doe',
callerNumber: '+1 234 567 890',
avatar: 'https://i.pravatar.cc/200',
duration: const Duration(seconds: 30), // Auto-timeout β missed call
extra: {'meetingId': 'abc'}, // Your custom data (passed back in events)
android: AndroidCallKitParams(
backgroundGradient: GradientConfig(
colors: ['#1A1A2E', '#16213E', '#0F3460'],
),
),
ios: IOSCallKitParams(
handleType: 'phoneNumber',
),
),
);
What happens:
- Android β Foreground service starts β high-priority notification β full-screen
IncomingCallActivity(even on lock screen) - iOS β Native CallKit UI with caller name and accept/decline
2. Listen to Events
callKit.onEvent.listen((event) {
switch (event.action) {
case CallKitAction.accept:
// User accepted β connect your VoIP call here
print('Accepted call ${event.callId}');
break;
case CallKitAction.decline:
// User declined β reject on your server
print('Declined call ${event.callId}');
break;
case CallKitAction.timeout:
// No answer within duration
print('Call timed out');
break;
case CallKitAction.dismissed:
// You called dismiss() (remote cancel)
print('Call dismissed');
break;
case CallKitAction.callback:
// User tapped "Call Back" on missed call notification
print('Callback requested for ${event.callId}');
break;
default:
break;
}
});
3. Dismiss a Call
When the remote side cancels or hangs up:
// Dismiss a specific call
await callKit.dismiss('call-123');
// Dismiss all active calls
await callKit.dismissAll();
4. Outgoing Calls
// Start an outgoing call
await callKit.startCall(
CallKitParams(
id: 'out-456',
callerName: 'Jane Smith',
callerNumber: '+1 987 654 321',
android: AndroidCallKitParams(),
ios: IOSCallKitParams(),
),
);
// When media connects (WebRTC peer connection / Twilio connected / Agora joined)
await callKit.setCallConnected('out-456');
// End the call
await callKit.endCall('out-456');
// Or end all calls at once
await callKit.endAllCalls();
What happens:
- Android β Ongoing call notification with "End Call" button. On
setCallConnected, notification shows a duration timer. - iOS β CallKit outgoing call UI via
CXStartCallAction.
5. Missed Call Notification
await callKit.showMissedCallNotification(
CallKitParams(
id: 'missed-789',
callerName: 'Missed Caller',
callerNumber: '+1 111 222 333',
missedCallNotification: NotificationParams(
showNotification: true,
subtitle: 'Missed Call',
showCallback: true,
callbackText: 'Call Back',
),
android: AndroidCallKitParams(),
ios: IOSCallKitParams(),
),
);
// Clear it later
await callKit.clearMissedCallNotification('missed-789');
Tapping "Call Back" fires
CallKitAction.callback.
6. Background Handler
Process call events even when your app is killed / terminated:
// Must be top-level or static β NOT an instance method or closure
@pragma('vm:entry-point')
Future<void> backgroundCallHandler(CallKitEvent event) async {
print('Background event: ${event.action} for ${event.callId}');
// e.g. log to analytics, notify your server
}
void main() {
WidgetsFlutterBinding.ensureInitialized();
// Register BEFORE runApp()
IncomingCallKit.registerBackgroundHandler(backgroundCallHandler);
runApp(MyApp());
}
The handler runs in a headless
FlutterEngine. It won't have access to your app's widget tree or state.
7. Permissions
final callKit = IncomingCallKit.instance;
// ββ Notification permission (Android 13+) ββ
final hasNotif = await callKit.hasNotificationPermission();
if (!hasNotif) {
final granted = await callKit.requestNotificationPermission();
print('Notification permission: $granted');
}
// ββ Full-screen intent (Android 14+) ββ
final canFullScreen = await callKit.canUseFullScreenIntent();
if (!canFullScreen) {
await callKit.requestFullIntentPermission();
// Opens system settings β user must grant manually
}
On iOS, both return sensible defaults. Notification permission uses the standard iOS authorization flow.
8. OEM Autostart (Android)
Chinese OEMs (Xiaomi, OPPO, Vivo, Huawei, Realme, OnePlus, Samsung) aggressively kill background processes:
final available = await callKit.isAutoStartAvailable();
if (available) {
// Show a dialog explaining why autostart is needed, then:
await callKit.openAutoStartSettings();
}
Supported manufacturers:
| Manufacturer | Settings screen |
|---|---|
| Xiaomi / Redmi | MIUI Autostart Manager |
| OPPO / Realme | ColorOS Startup Manager |
| Vivo / iQOO | Background App Manager |
| Huawei / Honor | Startup Manager / Protected Apps |
| Samsung | Battery Optimization |
| OnePlus | Chain Launch Manager |
Returns
falseon iOS and non-OEM Android devices.
9. iOS VoIP Token
final token = await callKit.getDevicePushTokenVoIP();
print('VoIP token: $token');
// Send this to your server for PushKit delivery
// Listen for token updates:
callKit.onEvent.listen((event) {
if (event.action == CallKitAction.voipTokenUpdated) {
final newToken = event.extra?['token'] as String?;
print('New VoIP token: $newToken');
}
});
Returns an empty string on Android.
10. Active Calls
final activeCalls = await callKit.getActiveCalls();
print('Active call IDs: $activeCalls'); // ['call-123', 'out-456']
π¨ Customization
Important: All customization is done from Dart. You never need to touch Kotlin, Swift, or XML files.
Android UI Customization
The Android call screen is a fully native Activity, but every visual element is controlled from Dart via AndroidCallKitParams:
AndroidCallKitParams(
// ββ Background ββ
backgroundColor: '#1B1B2F', // Solid color (hex string)
// backgroundGradient: ..., // OR gradient (see below) β mutually exclusive
// backgroundImageUrl: '...', // Background image URL
// ββ Avatar ββ
avatarSize: 96, // Diameter in dp
avatarBorderColor: '#FFFFFF',
avatarBorderWidth: 3.0,
avatarPulseAnimation: true, // Breathing ring animation
// ββ Initials fallback (when no avatar URL) ββ
initialsBackgroundColor: '#3A3A5C',
initialsTextColor: '#FFFFFF',
// ββ Text ββ
callerNameColor: '#FFFFFF',
callerNameFontSize: 28,
callerNumberColor: '#B3FFFFFF',
callerNumberFontSize: 16,
statusText: 'Incoming Call',
statusTextColor: '#80FFFFFF',
// ββ Buttons ββ
acceptButtonColor: '#4CAF50', // Green
declineButtonColor: '#F44336', // Red
buttonSize: 64, // Diameter in dp
// ββ Gestures ββ
enableSwipeGesture: true, // Swipe up = accept, down = decline
swipeThreshold: 120, // Pixels needed to trigger
// ββ Sound ββ
ringtonePath: 'system_ringtone_default', // or custom resource name
enableVibration: true,
vibrationPattern: [0, 1000, 1000], // [delay, vibrate, sleep, ...]
// ββ Behavior ββ
showOnLockScreen: true,
channelName: 'Incoming Calls',
showCallerIdInNotification: true,
)
Gradient Backgrounds
Use GradientConfig instead of a solid color:
// ββ Linear gradient (top to bottom) ββ
AndroidCallKitParams(
backgroundGradient: GradientConfig(
type: 'linear',
colors: ['#1A1A2E', '#16213E', '#0F3460'],
stops: [0.0, 0.5, 1.0], // Optional
begin: {'x': 0.5, 'y': 0.0}, // Top center
end: {'x': 0.5, 'y': 1.0}, // Bottom center
),
)
// ββ Radial gradient ββ
AndroidCallKitParams(
backgroundGradient: GradientConfig(
type: 'radial',
colors: ['#2D1B69', '#11001C'],
center: {'x': 0.5, 'y': 0.3},
radius: 0.8,
),
)
backgroundColorandbackgroundGradientare mutually exclusive. Setting both throwsAssertionErrorin debug mode. If neither is set, the default gradient['#1A1A2E', '#16213E', '#0F3460']is used.
iOS CallKit Customization
iOS uses Apple's native CallKit β customization is limited to what Apple allows:
IOSCallKitParams(
iconName: 'CallKitLogo', // 40Γ40pt asset catalog image
handleType: 'phoneNumber', // 'phoneNumber', 'email', or 'generic'
supportsVideo: false,
maximumCallGroups: 2,
maximumCallsPerCallGroup: 1,
ringtonePath: 'MyRingtone.caf',
supportsDTMF: true,
supportsHolding: false,
)
Missed Call Notification Config
NotificationParams(
showNotification: true, // Enable the notification
subtitle: 'Missed Call', // Notification body text
showCallback: true, // Show "Call Back" action button
callbackText: 'Call Back', // Button label
)
Flutter Widget (Foreground)
For foreground use, push the included Flutter widget as a route:
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => IncomingCallScreen(
params: params,
onAccept: () {
Navigator.pop(context);
// Connect your VoIP call
},
onDecline: () {
Navigator.pop(context);
// Reject the call
},
),
),
);
Renders the same gradient, avatar, pulse animation, and swipe buttons β but as a Flutter widget inside your app's navigation.
π VoIP Integration Guides
This plugin provides the call UI layer. Your VoIP SDK provides the audio/video layer. Here's how to connect them.
Twilio Voice
// 1. Receive push notification from Twilio
// (via firebase_messaging or your push handler)
// 2. Show the call UI
await callKit.show(CallKitParams(
id: twilioCallSid,
callerName: callerIdentity,
callerNumber: fromNumber,
android: AndroidCallKitParams(),
ios: IOSCallKitParams(),
));
// 3. Handle events
callKit.onEvent.listen((event) {
switch (event.action) {
case CallKitAction.accept:
// Accept the Twilio call
await twilioVoice.call.answer();
break;
case CallKitAction.decline:
// Reject the Twilio call
await twilioVoice.call.reject();
break;
case CallKitAction.callEnded:
// End the Twilio call
await twilioVoice.call.disconnect();
break;
default:
break;
}
});
// 4. When Twilio remote party disconnects:
twilioVoice.call.onDisconnected(() {
callKit.dismiss(twilioCallSid);
});
Agora Voice/Video
// 1. Receive signaling message (via FCM, WebSocket, etc.)
// 2. Show the call UI
await callKit.show(CallKitParams(
id: agoraChannelName,
callerName: callerName,
avatar: callerAvatar,
android: AndroidCallKitParams(),
ios: IOSCallKitParams(),
));
// 3. Handle events
callKit.onEvent.listen((event) async {
switch (event.action) {
case CallKitAction.accept:
// Join the Agora channel
await agoraEngine.joinChannel(
token: agoraToken,
channelId: agoraChannelName,
uid: localUid,
);
await callKit.setCallConnected(agoraChannelName);
break;
case CallKitAction.decline:
// Notify server you declined
await signaling.rejectCall(agoraChannelName);
break;
case CallKitAction.callEnded:
await agoraEngine.leaveChannel();
break;
default:
break;
}
});
// 4. When remote user leaves:
agoraEngine.onUserOffline = (uid, reason) {
callKit.endCall(agoraChannelName);
};
WebRTC (SIP / Location-based)
// 1. Receive SIP INVITE or signaling push
// 2. Show the call UI
await callKit.show(CallKitParams(
id: sessionId,
callerName: sipCaller.displayName,
callerNumber: sipCaller.uri,
android: AndroidCallKitParams(),
ios: IOSCallKitParams(),
));
// 3. Handle events
callKit.onEvent.listen((event) async {
switch (event.action) {
case CallKitAction.accept:
// Create WebRTC peer connection, add tracks, send SDP answer
await peerConnection.setRemoteDescription(offer);
final answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer);
await signaling.sendAnswer(answer);
await callKit.setCallConnected(sessionId);
break;
case CallKitAction.decline:
await signaling.sendReject(sessionId);
break;
case CallKitAction.audioSessionActivated:
// iOS only β configure audio track NOW
await peerConnection.setAudioEnabled(true);
break;
default:
break;
}
});
Firebase Cloud Messaging (FCM)
Use FCM as the push transport to trigger the call UI:
// In your FCM background handler:
@pragma('vm:entry-point')
Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
if (message.data['type'] == 'incoming_call') {
await IncomingCallKit.instance.show(
CallKitParams(
id: message.data['callId'],
callerName: message.data['callerName'],
callerNumber: message.data['callerNumber'],
avatar: message.data['avatar'],
android: AndroidCallKitParams(),
ios: IOSCallKitParams(),
),
);
}
}
// For iOS, use PushKit VoIP pushes instead of FCM for reliable wake-up:
final voipToken = await callKit.getDevicePushTokenVoIP();
// Send voipToken to your server β server sends PushKit push β plugin shows CallKit
iOS important: On iOS 13+, Apple requires you to report a CallKit call for every PushKit push. This plugin handles that automatically in
pushRegistry:didReceiveIncomingPushWith.
π Event Reference
All events arrive through callKit.onEvent as CallKitEvent objects:
| Action | Platform | Fires when |
|---|---|---|
accept |
Both | User tapped accept or swiped up |
decline |
Both | User tapped decline or swiped down |
timeout |
Both | No answer within duration |
dismissed |
Both | Call cancelled via dismiss() (remote cancel) |
callback |
Both | User tapped "Call Back" on missed call notification |
callStart |
Both | Outgoing call started via startCall() |
callConnected |
Both | setCallConnected() acknowledged |
callEnded |
Both | Call ended (outgoing) via endCall() |
audioSessionActivated |
iOS | Audio session ready β configure WebRTC audio here |
toggleHold |
iOS | User toggled hold in CallKit UI |
toggleMute |
iOS | User toggled mute in CallKit UI |
toggleDmtf |
iOS | User sent DTMF tone |
toggleGroup |
iOS | User toggled call group |
voipTokenUpdated |
iOS | PushKit VoIP token changed |
class CallKitEvent {
final CallKitAction action; // The event type (enum)
final String callId; // Which call this belongs to
final Map<String, dynamic>? extra; // Your custom data + event-specific data
}
π API Reference
IncomingCallKit.instance
| Method | Returns | Description |
|---|---|---|
show(CallKitParams) |
Future<void> |
Show incoming call UI |
dismiss(String callId) |
Future<void> |
Dismiss a specific call (remote cancel) |
dismissAll() |
Future<void> |
Dismiss all active calls |
startCall(CallKitParams) |
Future<void> |
Start an outgoing call |
setCallConnected(String) |
Future<void> |
Mark outgoing call as connected |
endCall(String callId) |
Future<void> |
End a specific call |
endAllCalls() |
Future<void> |
End all active calls |
showMissedCallNotification(CallKitParams) |
Future<void> |
Show missed call notification |
clearMissedCallNotification(String) |
Future<void> |
Remove a missed call notification |
onEvent |
Stream<CallKitEvent> |
Stream of all call lifecycle events |
canUseFullScreenIntent() |
Future<bool> |
Check full-screen permission (Android) |
requestFullIntentPermission() |
Future<void> |
Open settings for full-screen (Android) |
hasNotificationPermission() |
Future<bool> |
Check notification permission |
requestNotificationPermission() |
Future<bool> |
Request notification permission |
isAutoStartAvailable() |
Future<bool> |
Check OEM autostart settings (Android) |
openAutoStartSettings() |
Future<void> |
Open OEM autostart settings (Android) |
getDevicePushTokenVoIP() |
Future<String> |
Get PushKit VoIP token (iOS) |
getActiveCalls() |
Future<List<String>> |
Get all active call IDs |
IncomingCallKit.registerBackgroundHandler(handler)
| Parameter | Type | Description |
|---|---|---|
handler |
Future<void> Function(CallKitEvent) |
Top-level or static function with @pragma('vm:entry-point') |
π Architecture
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Dart Layer β
β β
β IncomingCallKit (singleton) β
β β β
β βΌ β
β IncomingCallKitPlatform (abstract) β
β β β
β βΌ β
β IncomingCallKitMethodChannel β
β ββ MethodChannel: "com.ashiquali.incoming_call_kit/methods" β
β ββ EventChannel: "com.ashiquali.incoming_call_kit/events" β
βββββββββββββββββββββββ¬βββββββββββββββββββββββ¬ββββββββββββββββββββββββ
β β
βββββββββββββββββββΌβββββββββ ββββββββββββΌβββββββββββββββ
β Android (Kotlin) β β iOS (Swift) β
β β β β
β IncomingCallKitPlugin β β IncomingCallKitPlugin β
β ββ IncomingCallService β β ββ CXProviderDelegate β
β ββ IncomingCallActivity β β ββ PKPushRegistryDel. β
β ββ AnswerTrampoline β β ββ UNNotificationDel. β
β ββ NotificationBuilder β β ββ FlutterStreamHandler β
β ββ CallKitEventBus β β β
β ββ CallKitConfigStore β β Frameworks: β
β ββ CallKitRingtoneManagerβ β - CallKit β
β ββ BackgroundCallHandlerβ β - PushKit β
β ββ OemAutostartHelper β β - AVFoundation β
β β β - UserNotifications β
ββββββββββββββββββββββββββββ ββββββββββββββββββββββββββββ
Key design decisions:
- EventBus over LocalBroadcastManager β
LocalBroadcastManageris deprecated. The in-processCallKitEventBusis thread-safe and lifecycle-aware. - Pending event replay β Events fired while the Flutter engine is dead are persisted to
SharedPreferencesand replayed when the engine reattaches. - Per-call notification IDs β Derived from
callId.hashCode(), supporting multiple simultaneous calls. - Foreground service fallback β On Android 12+, if
startForegroundService()throwsForegroundServiceStartNotAllowedException, the plugin falls back to notification-only. CallStyleon API 31+ β Native Android call notification treatment with big accept/decline buttons.- No
SYSTEM_ALERT_WINDOWβ Full-screen lock-screen display viaUSE_FULL_SCREEN_INTENT+ foreground service.
π§ Troubleshooting
Call screen doesn't show on lock screen (Android 14+)
Full-screen intent permission is required. Check and request:
if (!await callKit.canUseFullScreenIntent()) {
await callKit.requestFullIntentPermission(); // Opens system settings
}
Calls not received when app is killed (Xiaomi, OPPO, Vivo)
- Guide users to enable autostart:
if (await callKit.isAutoStartAvailable()) { await callKit.openAutoStartSettings(); } - Register a background handler:
IncomingCallKit.registerBackgroundHandler(myHandler);
No audio after accepting call on iOS
Listen for audioSessionActivated before configuring WebRTC audio:
callKit.onEvent.listen((event) {
if (event.action == CallKitAction.audioSessionActivated) {
// Configure your WebRTC / Twilio / Agora audio NOW
}
});
Duplicate notifications on Android
Each call must have a unique id. The plugin derives notification IDs from callId.hashCode().
Events not received after app restart
Events from killed state are stored and replayed automatically when you listen to onEvent. Subscribe early β in initState of your root widget.
Android 15 Play Store warning
The plugin declares PROPERTY_SPECIAL_USE_FGS_SUBTYPE with value incoming_voip_call in the manifest. No action needed.
iOS PushKit requirement
On iOS 13+, Apple requires a CallKit call for every PushKit VoIP push. The plugin handles this in pushRegistry:didReceiveIncomingPushWith: β just make sure your push payload includes id, callerName, and callerNumber keys.
π Connect
License
See LICENSE for details.