GitHub project

Silent push

New in v5.2.0

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 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:

KeyValue
CIO-Background-Delivery-IDIdentifies the delivery
CIO-Background-Delivery-TokenThe 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:

DART
@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:

DART
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_messaging runs onBackgroundMessage. This isolate shares nothing with the rest of your app and has no UI. Plugins work there, but CustomerIO.instance doesn’t, because CustomerIO.initialize hasn’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

  1. In Xcode, add the Background Modes capability and check Remote notifications. Without it, iOS doesn’t wake your app for a silent push.
  2. Keep CioAppDelegateWrapper as your app delegate. The plugin receives silent pushes from it.
  3. Register plugins in application(_:didFinishLaunchingWithOptions:) with GeneratedPluginRegistrant.register(with: self), so your Dart code can run when iOS launches your app in the background.
  4. 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.

DART
@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:

  • onBackgroundMessage takes a top-level or static function annotated with @pragma('vm:entry-point'), registered before runApp.
  • 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.onBackgroundMessageReceived works there without CustomerIO.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.
Updated November 13, 2026