> This page is part of the [Customer.io documentation](https://docs.customer.io). For the complete index, see [llms.txt](https://docs.customer.io/llms.txt).
> Last updated: November 13, 2026
> To take actions in Customer.io from an agent, use the [Customer.io CLI](https://docs.customer.io/ai/cli/get-started.md) or the [MCP server](https://docs.customer.io/ai/mcp/get-started.md).

# 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](https://docs.customer.io/messaging/channels/push/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](#customerio-handles-push) with `CustomerIO.pushMessaging.setBackgroundPushHandler`.
*   If `firebase_messaging` handles push, your existing [`firebase_messaging` handlers](#firebase-messaging-handles-push) 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_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](https://docs.customer.io/integrations/sdk/flutter/push-notifications/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](#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.
