When a Stringee call is made, Stringee Server sends a push message to the receiving client. If StringeeClient is already connected, the incoming call is delivered through StringeeClientEvents.incomingCall or incomingCall2, and the pushed message can be ignored. If the client is disconnected or the app process is not running, use the push message to restore the connection; the incoming call is then delivered through the Stringee event stream.


This tutorial describes the Stringee registration flow. Follow the current setup guide of each push or incoming-call UI plugin used by your application for its native configuration.
Stringee uses Firebase Cloud Messaging (FCM) on Android and VoIP notifications through APNs PushKit on iOS. Obtain the platform token, connect StringeeClient, then register the token by calling StringeeClient.registerPush.
The examples use stringee_plugin 1.3.2 or later, which requires Android API 21 or later and iOS 13.0 or later.
PushKit wakes the application for an incoming VoIP call. CallKit displays and coordinates the system calling UI. Apple requires an incoming VoIP push to be reported to CallKit promptly; do not use VoIP pushes for non-call notifications.
For more information, see CallKit and PushKit.
On Stringee Dashboard, go to Push Notification, select your project, and create or update an iOS application with the exact bundle identifier used by your app.

Create an APNs authentication key in Apple Developer -> Certificates, Identifiers & Profiles -> Keys. Record the Key ID and Team ID, then upload the original .p8 file with those values to the iOS push application on Stringee Dashboard. Do not bundle the .p8 file in the app or commit it to source control.



Use a plugin that supports both PushKit token delivery and native CallKit presentation. One possible integration is flutter_callkit_incoming:
$ flutter pub add flutter_callkit_incoming
Follow the plugin's current iOS setup, including its AppDelegate PushKit delegate. Enable the following capabilities for the iOS target:


The native PushKit handler must display CallKit and invoke the PushKit completion handler within Apple's required time, including when the Dart isolate has not started.
Obtain the VoIP PushKit token and register it after StringeeClient connects:
import 'package:flutter/foundation.dart';
import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart';
import 'package:stringee_plugin/stringee_plugin.dart';
final StringeeClient client = StringeeClient();
Future<void> registerIosVoipPush() async {
final dynamic value =
await FlutterCallkitIncoming.getDevicePushTokenVoIP();
final String token = value?.toString() ?? '';
if (token.isEmpty) {
return;
}
final result = await client.registerPush(
token,
isProduction: kReleaseMode,
isVoip: true,
);
print("Register VoIP push: ${result['message']}");
}
isVoip: true because this is a PushKit VoIP token.isProduction must match the APNs environment used to sign the app. kReleaseMode is suitable for the usual Debug/Release setup; use your explicit environment configuration when it differs.When StringeeClientEvents.incomingCall or incomingCall2 is delivered, call initAnswer on the received StringeeCall/StringeeCall2 and associate that object with the same CallKit identifier reported by the native PushKit handler. Answer or reject through the Stringee call object, not through the push payload.
Create a Firebase project in the Firebase console, add your Android application, and download google-services.json.
In Firebase Project Settings -> General, copy the Firebase project ID. In Service accounts, generate a Firebase Admin SDK private key. On Stringee Dashboard, create or update the Android push application with the project ID and package name, then upload the service-account JSON key. Keep that key private.
Install FlutterFire Core and Messaging, then configure the project:
$ flutter pub add firebase_core firebase_messaging
$ flutterfire configure
Initialize Firebase before registering the background handler and starting the app:
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'firebase_options.dart';
@pragma('vm:entry-point')
Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
await initializeLocalNotifications();
await handleStringeePush(message);
}
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
await initializeLocalNotifications();
FirebaseMessaging.onBackgroundMessage(
firebaseMessagingBackgroundHandler,
);
runApp(const MyApp());
}
Retrieve and register the FCM token after Stringee connects:
Future<void> registerAndroidPush(StringeeClient client) async {
final String? token = await FirebaseMessaging.instance.getToken();
if (token == null || token.isEmpty) {
return;
}
final result = await client.registerPush(token);
print("Register push: ${result['message']}");
}
On Android, isProduction and isVoip are ignored. Register every replacement token:
final StreamSubscription<String> tokenSubscription =
FirebaseMessaging.instance.onTokenRefresh.listen((token) {
client.registerPush(token);
});
Handle foreground messages in the main isolate. The background handler must remain a top-level function annotated with @pragma('vm:entry-point').
final StreamSubscription<RemoteMessage> messageSubscription =
FirebaseMessaging.onMessage.listen(handleStringeePush);
Future<void> handleStringeePush(RemoteMessage message) async {
if (!message.data.containsKey('stringeePushNotification')) {
return;
}
final dynamic decoded = jsonDecode(message.data['data']!);
final Map<String, dynamic> callData =
Map<String, dynamic>.from(decoded as Map);
final String status = callData['callStatus']?.toString() ?? '';
if (status == 'started') {
await showIncomingCallNotification(callData);
connectStringeeIfNeeded();
} else if (status == 'answered' || status == 'ended') {
await cancelIncomingCallNotification(callData['callId'].toString());
}
}
A Stringee FCM message contains a data payload similar to:
{
"stringeePushNotification": "1.0",
"type": "CALL_EVENT",
"data": "{\"callId\":\"CALL_ID\",\"serial\":1,\"callStatus\":\"started\",\"from\":{\"number\":\"user2\",\"alias\":\"User 2\"},\"to\":{\"number\":\"user1\",\"alias\":\"User 1\"},\"projectId\":1234}"
}
Use a notification library such as flutter_local_notifications:
$ flutter pub add flutter_local_notifications
Declare POST_NOTIFICATIONS and USE_FULL_SCREEN_INTENT in AndroidManifest.xml. On Android 13 or later, request notification permission at runtime. Full-screen intents are reserved for urgent calling or alarm experiences and can be disabled by the user on recent Android versions; always support a normal high-priority notification fallback.
Initialize the plugin using its current named-argument API:
final FlutterLocalNotificationsPlugin localNotifications =
FlutterLocalNotificationsPlugin();
Future<void> initializeLocalNotifications() async {
const settings = InitializationSettings(
android: AndroidInitializationSettings('@drawable/ic_noti'),
);
await localNotifications.initialize(
settings: settings,
onDidReceiveNotificationResponse: (response) {
// Route response.actionId to the active Stringee call object.
},
);
final android = localNotifications
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>();
await android?.requestNotificationsPermission();
}
Display a high-priority incoming-call notification:
Future<void> showIncomingCallNotification(
Map<String, dynamic> callData,
) async {
final Map<String, dynamic> from =
Map<String, dynamic>.from(callData['from'] as Map);
final String caller =
from['alias']?.toString() ?? from['number']?.toString() ?? 'Unknown';
const androidDetails = AndroidNotificationDetails(
'incoming_calls',
'Incoming calls',
channelDescription: 'Incoming Stringee calls',
importance: Importance.max,
priority: Priority.high,
category: AndroidNotificationCategory.call,
ongoing: true,
autoCancel: false,
fullScreenIntent: true,
actions: <AndroidNotificationAction>[
AndroidNotificationAction(
'answer',
'Answer',
showsUserInterface: true,
semanticAction: SemanticAction.call,
),
AndroidNotificationAction('reject', 'Reject'),
],
);
await localNotifications.show(
id: callData['callId'].toString().hashCode,
title: 'Incoming call',
body: 'Call from $caller',
notificationDetails: const NotificationDetails(
android: androidDetails,
),
payload: callData['callId'].toString(),
);
}
Future<void> cancelIncomingCallNotification(String callId) async {
await localNotifications.cancel(id: callId.hashCode);
}
Reconnect and obtain the real StringeeCall/StringeeCall2 before answering or rejecting; the push payload is not a call object.
When the user signs out, unregister the active token:
final String? token = await FirebaseMessaging.instance.getToken();
if (token != null && token.isNotEmpty) {
await client.unregisterPush(token);
}
You can view the latest example on GitHub: call_sample