Silent push
A silent push wakes your app in the background and hands it the data set on the message, without showing anything to the person. Use this page to set your app up to receive silent pushes.
How it works
When you send a silent push, the device wakes your app in the background, and your Dart code gets the key/value data set on the message. The SDK reports Delivered when the push arrives, before your code runs. A silent push never shows anything, so the SDK never reports Opened.
Silent push needs version 5.2.0 or later of the customer_io plugin. Customer.io sends silent pushes to every iOS and Android device a message targets, and doesn’t check which SDK version the device uses.
Handle silent pushes in one place, depending on what receives push in your app:
- If Customer.io handles push, register a Dart handler with
CustomerIO.pushMessaging.setBackgroundPushHandler. - If
firebase_messaginghandles push, your existingfirebase_messaginghandlers receive silent pushes.
If your app does both, your Dart code runs twice for every silent push.
What a silent push contains
A silent push carries your data as text keys and values, plus two keys that Customer.io adds:
| Key | Value |
|---|---|
CIO-Background-Delivery-ID | Identifies the delivery |
CIO-Background-Delivery-Token | The device token the push was sent to |
Customer.io handles push
Use this route when Customer.io’s FirebaseMessagingService receives messages on Android and CioAppDelegateWrapper is your app delegate on iOS.
Write a top-level or static handler annotated with @pragma('vm:entry-point'), and register it at the start of main, before your other await calls:
@pragma('vm:entry-point')
Future<void> onBackgroundPush(CustomerIOBackgroundPush push) async {
// push.data holds the keys and values set on the message.
// Key your work on push.deliveryId: the same push can arrive more than once.
await refreshAccount(push.data['account']);
}
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await CustomerIO.pushMessaging.setBackgroundPushHandler(onBackgroundPush);
// Firebase, CustomerIO.initialize, runApp, and the rest of your setup.
}Your handler gets your data without the two Customer.io keys. The plugin remembers your handler across launches. On Android, a push that starts your app runs the handler without running your main. On iOS, the push waits for main to register the handler again.
To stop receiving silent pushes, call setBackgroundPushHandler(null). A later version of your app that no longer registers a handler also stops them, after the first push that finds no handler. On iOS, that’s a push that waits 25 seconds without main registering a handler. On Android, that’s a push whose handler function is no longer in your app.
If you turn off automatic push tracking, report Delivered yourself from an isolate where CustomerIO.initialize has run:
CustomerIO.instance.trackMetric(
deliveryID: push.deliveryId,
deviceToken: push.deliveryToken,
event: MetricEvent.delivered,
);Android
You don’t need to do anything else on Android. The SDK runs your handler from a WorkManager job:
- On the main isolate of a running Flutter engine that registered the handler, like your app’s UI engine.
- Otherwise, on a background isolate that the plugin starts, the same way
firebase_messagingrunsonBackgroundMessage. This isolate shares nothing with the rest of your app and has no UI. Plugins work there, butCustomerIO.instancedoesn’t, becauseCustomerIO.initializehasn’t run in that isolate.
The job keeps your app’s process running until your handler finishes, and Android stops it after 10 minutes. If the process ends or your app’s engine goes away first, WorkManager runs the job again, so your handler can see the same push twice.
iOS
- In Xcode, add the Background Modes capability and check Remote notifications. Without it, iOS doesn’t wake your app for a silent push.
- Keep
CioAppDelegateWrapperas your app delegate. The plugin receives silent pushes from it. - Register plugins in
application(_:didFinishLaunchingWithOptions:)withGeneratedPluginRegistrant.register(with: self), so your Dart code can run when iOS launches your app in the background. - If you use FCM with Firebase’s app delegate proxy on, which is Firebase’s default, your app needs GoogleUtilities 8.1.5 or later. With earlier versions, silent pushes never reach the SDK.
Your handler runs on the main isolate. The SDK tells iOS that the push is complete when your handler finishes, or after 25 seconds, within the 30 seconds iOS allows.
Once your app registers a handler, the plugin takes Customer.io silent pushes from CioAppDelegateWrapper, so a didReceiveRemoteNotification override in your app delegate doesn’t see them. firebase_messaging still gets them through Firebase’s app delegate proxy, unless you turn the proxy off.
firebase_messaging handles push
Use this route when firebase_messaging receives your messages and forwards them to Customer.io, as described in multiple push providers. Silent pushes reach FirebaseMessaging.onBackgroundMessage and onMessage without any changes on your side. message.notification is null, and message.data holds your keys plus the two Customer.io keys.
@pragma('vm:entry-point')
Future<void> firebaseBackgroundHandler(RemoteMessage message) async {
if (message.data['CIO-Background-Delivery-ID'] != null) {
// A Customer.io silent push: read your keys from message.data.
await refreshAccount(message.data['account']);
}
await CustomerIO.pushMessaging.onBackgroundMessageReceived(message.toMap());
}Keep forwarding every message to onBackgroundMessageReceived or onMessageReceived, and don’t show a notification for a silent push yourself. On Android, the SDK reports Delivered for a silent push, shows nothing, and resolves to true. On iOS, the method resolves to true, and CioAppDelegateWrapper reports Delivered. On iOS, add the Remote notifications background mode, as described in iOS.
firebase_messaging sets the rules for its background handler:
onBackgroundMessagetakes a top-level or static function annotated with@pragma('vm:entry-point'), registered beforerunApp.- On Android, the handler runs in a separate isolate with no shared state and no UI. Call
Firebase.initializeApp()there before you use Firebase.CustomerIO.pushMessaging.onBackgroundMessageReceivedworks there withoutCustomerIO.initialize. - On iOS, the handler runs on the main isolate.
- Firebase gives each message only a short time on Android, and iOS allows about 30 seconds. Hand longer work to a background job.
What to plan for
Apple and Google decide when to deliver a silent push, and don’t always deliver it:
- Repeats: The same push can arrive more than once. Use the delivery ID to skip work you already did.
- Delays: A push can arrive late, like while Android holds messages for an idle device.
- Offline devices: After a device has been offline, iOS delivers only the newest push.
- Force-quit and force-stopped apps: iOS doesn’t wake an app the person force-quit, and Android doesn’t deliver to an app the person force-stopped.