# Flutter SDK Documentation

Generated on: 9/9/2026

This documentation bundle contains the complete Flutter SDK documentation for use with LLMs and offline reference.

---

## Quick start guide

**Source:** /integrations/sdk/flutter/quick-start-guide

# Quick start guide

Before you can take advantage of our SDK, you need to install and initialize the SDK.

Our MCP server can help you get started

Our MCP server includes SDK-installation tools that can help you get integrated quickly with Customer.io and troubleshoot any issues you might have. See [Set up Customer.io MCP](/ai/mcp/get-started/) to get started.

## Setup process overview

Flutter lets you build native mobile apps using Dart. Our Flutter SDK helps you integrate Customer.io to `identify` profiles, `track` their activity, and send both push notifications and in-app messages.

1.  [Install and initialize the SDK.](#install-init)
2.  [Identify and Track](#identify-and-track)
3.  [Push Notifications](#push-notifications)
4.  [In-App](#in-app)

## 1\. Install the SDK

1.  In your project folder install the `customer_io` package:
    
    ```
    flutter pub add customer_io
    ```
    
    This adds a line to your package’s `pubspec.yaml`
    
    ```
    dependencies:
       customer_io: ^4.4.0
    ```
    
2.  Set up your project to support iOS and/or Android:
    
    iOSAndroid
    
    #### iOS
    
    1.  Set up your iOS dependencies:
        
        *   **CocoaPods**: Run `pod install --repo-update --project-directory=ios`.
        *   **SPM**: Make sure SPM is enabled in your Flutter project (see [Flutter docs ↗](https://docs.flutter.dev/packages-and-plugins/swift-package-manager/for-app-developers)), and run `flutter pub get`. The SDK is automatically resolved.
        
        For full iOS setup details, including the Notification Service Extension for rich push, see [Set up push notifications](/integrations/sdk/flutter/push-notifications/push-setup/).
        
    
    #### Android
    
    1.  Go to the Android subfolder and include [google-services-plugin ↗](https://developers.google.com/android/guides/google-services-plugin) by adding the following lines to the project-level `android/build.gradle` file:
        
        ```
        buildscript {
           repositories {
              // Add this line if it isn't already in your build file:
              google()  // Google's Maven repository
           }
        
           dependencies {
              // Add this line:
              classpath 'com.google.gms:google-services:<version-here>'  // Google Services plugin
           }
        }
        
        allprojects {
           repositories {
              // Add this line if it isn't already in your build file:
              google()  // Google's Maven repository
           }
        }
        ```
        
    2.  Add the following line to `android/app/build.gradle`:
        
        ```
        apply plugin: 'com.google.gms.google-services'  // Google Services plugin
        ```
        
    3.  Download `google-services.json` from your Firebase project and copy the file to `android/app/google-services.json`.
        
    
3.  Initialize the SDK:
    
    1.  Add your *CDP API key* and *site ID* to your configuration.
        
        *   **CDP API Key**: You’ll find this key in your [*Flutter* connection](/integrations/sdk/flutter/getting-started/auth/#get-your-cdp-api-key).
        *   **Site ID**: You’ll find this value in your workspace under **[Settings > Workspace Settings > API and webhook credentials](https://fly.customer.io/workspaces/last/settings/api_credentials)**.
    2.  Initialize the SDK in your app. In your `main.dart` file—or wherever you want to initialize the `CustomerIO` plugin—add the code below:
        
        ```
         import 'package:customer_io/customer_io.dart';
         import 'package:customer_io/customer_io_config.dart';
         import 'package:customer_io/customer_io_enums.dart';
        
        
         await CustomerIO.initialize(
          config: CustomerIOConfig(
            cdpApiKey: 'cdpApiKey',
            region: Region.us, // Replace with Region.EU if your Customer.io account is in the EU.
            inAppConfig: InAppConfig(siteId: 'siteId'),
          ),
        );
        ```
        
    3.  Run your application to ensure everything is set up correctly.
        

Also track location and geofences

Want to react when someone enters or leaves a real-world place—a store, a venue, a neighborhood? The SDK’s Location and Geofence modules do that. They need location permissions and some native setup, so they aren’t part of the steps above. See [geofencing](/integrations/sdk/flutter/tracking/geofencing/) to get started, or the [packages and configuration reference](/integrations/sdk/flutter/getting-started/packages-options/) for every option.

## 2\. Identify and Track

1.  Identify a user in your app using the `CustomerIO.identify` method. You *must* identify a user before you can send push notifications and personalized in-app messages.
    
    ```
    CustomerIO.instance.identify(userId: email, traits: {
    "name": user.displayName,
    "email": user.email,
    "age": user.age,
    });
    ```
    
2.  Track a custom event using the `CustomerIO.track` method. Events help you trigger personalized automations and track user activity.
    
    ```
    CustomerIO.instance.track(name: "add-to-cart", properties: {"product": "shoes", "price": "29.99"});
    ```
    
3.  Track screen views to trigger in-app messages associated with specific screens.
    
    ```
    CustomerIO.instance.screen(title: "screen-name", properties: {"property": "value"});
    ```
    

## 3\. Push Notifications

iOSAndroid

#### iOS

1.  Set up your push notification credentials in Customer.io: [Upload your Firebase Cloud Messaging server key](https://fly.customer.io/workspaces/last/settings/actions/push/ios) (.json format). Our Flutter SDK uses FCM for both iOS and Android push notifications.
2.  Request push notification permissions from the user. You can do this through [Firebase ↗](https://firebase.google.com/docs/cloud-messaging/flutter/receive) or any other package.
3.  To ensure that metrics are tracked, configure Background Modes. In Xcode, enable “Remote notifications” under Capabilities > Background Modes.

#### Android

1.  Set up your push notification credentials in Customer.io: [Upload your Firebase Cloud Messaging server key](https://fly.customer.io/workspaces/last/settings/actions/push/android) (.json format).
2.  Request push notification permissions from the user. You can do this through [Firebase ↗](https://firebase.google.com/docs/cloud-messaging/flutter/receive) or any other package.
3.  Ensure that you:
    1.  [Add your Google Firebase Cloud Messaging (FCM) key to Customer.io and enable push notifications for Android](https://docs.customer.io/messaging/push-developer-guide/#fcm-setup). Our Flutter SDK receives push notifications from FCM.
    2.  Add notification icon resources:
        *   Place a notification icon file named `ic_notification.png` in your drawable folders.
        *   Make sure your app’s `AndroidManifest.xml` has the proper FCM permissions.

## 4\. In-App

1.  To enable in-app messaging, all you need to do is add your site ID. Remember, you’ll find your site ID under **[Integrations > Customer.io API: Track](https://fly.customer.io/workspaces/last/settings/api_credentials)** in the *Connections* tab.
    
2.  Ensure that the SDK is initialized with the site ID in your app. You can call the `initialize` method from your components or services:
    
    ```
     import 'package:customer_io/customer_io.dart';
     import 'package:customer_io/customer_io_config.dart';
     import 'package:customer_io/customer_io_enums.dart';
    
    
     await CustomerIO.initialize(
     config: CustomerIOConfig(
         cdpApiKey: 'cdpApiKey',
         region: Region.us, // Replace with Region.EU if your Customer.io account is in the EU.
         inAppConfig: InAppConfig(siteId: 'siteId'),
         pushConfig: PushConfig(
         android: PushConfigAndroid(
             pushClickBehavior:
             PushClickBehaviorAndroid.activityPreventRestart,
         ),
         ),
     ),
     );
    ```

---

## Getting started > Auth

**Source:** /integrations/sdk/flutter/getting-started/auth

# Authentication

To use the SDK, you'll need to get two kinds of keys: A [*CDP API Key*](#get-your-cdp-api-key) to send data to Customer.io and a [*Site ID*](#get-your-site-id) that tells the SDK which workspace your messages come from.

## Get your *CDP API Key*

You’ll use a *CDP API Key* to initialize the SDK and send data to Customer.io. You’ll get this key when you set up your mobile app as a data in integration in Customer.io. If you haven’t already set up your integration in Customer.io, you’ll need to [do that first](#set-up-a-new-source).

1.  Go to *Integrations*.
2.  Select your Flutter integration in the *Overview* tab. If you don’t see a Flutter integration, you’ll need to [set it up](#set-up-a-new-source).
    
     ![the connections page, showing an Flutter source connected to a journeys destination](https://customer.io/images/cdp-flutter-connected-destination.png)
    
3.  Go to **Settings** and find your **CDP API Key**. Copy this key into your initialization call. If you’re upgrading from a previous version of the SDK, you should set the `siteId` that you used in previous versions as the `migrationSiteId` in your config.
    
     ![get your CDP API Key from your source's settings page](https://customer.io/images/cdp-flutter-source-api-key.png)
    
    ```
    CustomerIO.initialize(
       config: CustomerIOConfig(
          cdpApiKey: '<your CDP API Key>',
          //migrationSiteId is required if you're updating from a previous version
          migrationSiteId: '<your siteId>', 
          region: Region.us, // Replace with Region.EU if your Customer.io account is in the EU.
          autoTrackDeviceAttributes: true,
          inAppConfig: InAppConfig(siteId: '<your siteId>'),
       ),
    );
    ```
    

You're not done yet

You still need your [Site ID](https://fly.customer.io/env/last/settings/api_credentials) to initialize the `CioMessagingInApp` package and to support people updating your app from a previous version of Customer.io SDK. See [Get your Site ID](#get-your-site-id) below.

### Set up a new integration

If you don’t already have a write key, you’ll need to set up a new data in integration in Customer.io. The “integration” represents your app and the stream of data that you’ll send to Customer.io.

1.  Go to [*Integrations*](https://fly.customer.io/workspaces/last/journeys/integrations/all/overview) and click **Add Integration**.
2.  Select **Flutter**.
    
     ![set up your Flutter integration](https://customer.io/images/cdp-flutter-source.png)
    
3.  Enter a *Name* for your integration, like “My Flutter App”.
4.  We’ll present you with a code sample containing a `cdpApiKey` that you’ll use to initialize the SDK. Copy this key and keep it handy.
5.  Test your connection and click **Complete Setup**. Or, if you don’t want to test your implementation yet, **Save & Complete Later** and then click **Install Source** to finish the setup process. In this case, *Complete Later* simply means that we haven’t seen any data from your Flutter app yet.
    
     ![Set your name, get your CDP API Key, and click Complete Setup](https://customer.io/images/cdp-flutter-source-setup.png)
    

Remember, you can also [connect your Flutter app to services outside of Customer.io](/integrations/data-out/add-destination/)—like your analytics provider, data warehouse, or CRM.

## Get your Site ID

You’ll use your Site ID to send in-app messages from your workspace.

If you’re upgrading from a previous version, my can also set your Site ID as your `migrationSiteId`. This key is used to send remaining tasks to Customer.io when your audience updates your app.

1.  Go to and select **Workspace Settings** in the upper-right corner of the Customer.io app and go to [**API and Webhook Credentials**](https://fly.customer.io/workspaces/last/settings/api_credentials).
    
2.  Copy the **Site ID** for the set of credentials that you want to send your in-app messages from. If you don’t have a set of credentials, click **Create Tracking API Key**.
    
     ![find your site ID](https://customer.io/images/cdp-js-site-id.png)
    
3.  You’ll use this key to initialize the `inApp` package. If you’re upgrading from a previous version, you’ll also use it as your `migrationSiteId`.
    
    ```
    CustomerIO.initialize(
       config: CustomerIOConfig(
          cdpApiKey: '<your API Key>',
          //required if you're updating from a previous version
          migrationSiteId: '<your siteId>', 
          region: Region.us, // Replace with Region.EU if your Customer.io account is in the EU.
          autoTrackDeviceAttributes: true,
          inAppConfig: InAppConfig(siteId: '<your siteId>'),
       ),
    );
    ```
    

## Securing your credentials

To simplify things, code samples in our documentation sometimes show API keys directly in your code. But you don’t have to hard-code your keys in your app. You can use environment variables, management tools that handle secrets, or other methods to keep your keys secure if you’re concerned about security.

To be clear, the keys that you’ll use to initialize the SDK don’t provide read access to data in Customer.io; they only write data to Customer.io. A bad actor who found your credentials can’t use your keys to read data from our servers.

---

## Getting started > How it works

**Source:** /integrations/sdk/flutter/getting-started/how-it-works

# How it works

Before you can take advantage of our SDK, you need to install the module(s) you want to use, initialize the SDK, and understand the order of operations.

Our SDKs provide a ready-made integration to identify profiles that use mobile devices and send them notifications. Before you start using the SDK, you should understand a bit about how the SDK works with Customer.io.

Before a person logs into your app, any activity they perform is associated with an anonymous person in Customer.io. In this state, you can track their activity, but you can’t send them messages through Customer.io.

When someone logs into your app, you’ll send an `identify` call to Customer.io. This makes the profile eligible to receive messages and reconciles their anonymous activity to their identified profile in Customer.io.

You send messages to a profile through the Customer.io automation builder, broadcasts, etc. These messages are not stored on the device side. If you want to send an event-triggered automation to a mobile device, the mobile device user must be identified and have a connection such that it can send an event back to Customer.io and receive a message payload.

## Your app is a data source and Customer.io is a destination

Our SDK is a data in integration. It routes data from your app to both Customer.io *and* any other outbound services where you might use your mobile data. This makes it easy to use your app as a part of your larger data stack without using extra packages or code.

When you set up your app, you’ll integrate our SDK. But you’ll also determine where you want to route your data to—your Customer.io workspace *and* destinations outside of Customer.io.

## Minimum support requirements

To support the Customer.io SDK, you must:

*   Use Gradle 8.0 or later.
*   Use Android Gradle plugin version 8.0 or later (8.2+ recommended).
*   Use Kotlin 1.9.20 or later (2.0+ required if using Kotlin Multiplatform or K2-specific features).
*   Set iOS 13 or later as your minimum deployment target in XCode.
*   Have an Android device or emulator with Google Play Services enabled and a minimum OS version between Android 5.0 (API level 21) and Android 13.0 (API level 33).
*   Have an iOS 13+ device or Simulator to test your implementation. Remote sandbox APNs delivery requires an iOS 16+ Simulator on macOS 13+ running on Apple silicon or an Apple T2 Security Chip. Use a physical device for production APNs and device-specific behavior.

## The Processing Queue

The SDK automatically adds all calls to a queue system, and waits to perform these calls until certain criteria is met. This queue makes things easier, both for you and your users: it handles errors and retries for you (even when users lose connectivity), and it can save users’ battery life by batching requests.

The queue holds requests until any one of the following criteria is met:

*   There are 20 or more tasks in the queue.
*   30 seconds have passed since the SDK performed its last task.
*   The app is closed and re-opened.

For example, when you identify a new profile in your app using the SDK, you won’t see the created/updated profile immediately. You’ll have to wait for the SDK to meet any of the criteria above before the SDK sends a request to the Customer.io API. Then, if the request is successful, you’ll see your created/updated profile in your workspace.

---

## Getting started > Packages options

**Source:** /integrations/sdk/flutter/getting-started/packages-options

# Configuration options

The SDK consists of a few packages. You'll get the most value out of Customer.io when you use all our packages together, but this lets you omit packages for features you don't intend to use.

You’ll call configuration options before you initialize the SDK with `CustomerIOConfig`. When you initialize the SDK, you can pass configuration options. In most cases, you’ll want to stick with the defaults, but you might do things like change the `logLevel` when testing updates to your app.

```
CustomerIO.initialize(
  config: CustomerIOConfig(
    cdpApiKey: "YOUR_CDP_API_KEY", // Required
    migrationSiteId: "YOUR_SITE_ID", // Required to migrate from a previous version
    autoTrackDeviceAttributes: true,
    region: Region.us,
    logLevel: CioLogLevel.error
  )
);
```

Option

Type

Default

Description

`cdpApiKey`

string

**Required**: the key you’ll use to initialize the SDK and send data to Customer.io

`region`

`eu` or `us`

`us`

**Required if your account is in the EU region**.

`apiHost`

string

The domain you’ll proxy requests through. You’ll only need to set this (and `cdnHost`) if you’re [proxying requests](#proxying-requests).

`autoTrackDeviceAttributes`

boolean

`true`

Automatically gathers information about devices, like operating system, device locale, model, app version, etc

`cdnHost`

string

The domain you’ll fetch configuration settings from. You’ll only need to set this (and `apiHost`) if you’re [proxying requests](#proxying-requests).

`logLevel`

string

`error`

Sets the level of logs you can view from the SDK. Set to `debug` or `info` to see more logging output.

`migrationSiteId`

string

**Required if you’re updating from 1.x**: the credential for previous versions of the SDK. We use this key to send remaining tasks to Customer.io when your audience updates your app.

`screenViewUse`

`All` or `inApp`

`all`

`ScreenView.all` (Default): Screen events are sent to Customer.io. You can use these events to build segments, trigger automations, and target in-app messages.  
  
`ScreenView.InApp`: Screen view events not sent to Customer.io. You’ll *only* use them to target in-app messages based on page rules.

`trackApplicationLifecycleEvents`

boolean

`true`

Set to `false` if you don’t want the app to send [lifecycle events](/integrations/sdk/flutter/tracking/lifecycle-events/) like `Application Opened`

`inApp.siteId`

string

Used to initialize the `inApp` package, and determines the workspace your in-app messages come from.

`push.android.pushClickBehavior`

string

`activityPreventRestart`

One of `resetTaskStack`, `activityPreventRestart`, `activityNoFlags`; determines how to handle push clicks.

`locationConfig`

object

Enable [location tracking](/integrations/sdk/flutter/tracking/location/). Takes a `trackingMode` option using the `LocationTrackingMode` enum.

`geofenceConfig`

object

Enable [geofencing](/integrations/sdk/flutter/tracking/geofencing/). Enables the Location module automatically. Takes a `GeofenceConfig()` with an optional `locationMode` from the `GeofenceLocationMode` enum.

## Proxying requests

By default, requests go through our domain at `cdp.customer.io`. You can proxy requests through your own domain to provide a better privacy and security story, especially when submitting your app to app stores.

To proxy requests, you’ll need to set the `apiHost` and `cdnHost` properties in your `SDKConfigBuilder`. While these are separate settings, you should set them to the same URL.

While you need to initialize the SDK with a `cdpApiKey`, you can set this to any value you want. You only need to pass your actual key when you send requests from your server backend to Customer.io. If you want to secure requests to your proxy server, you can set the `cdpApiKey` to a value representing basic authentication credentials that you handle on your own. See [proxying requests](/integrations/data-in/proxying-requests/) for more information.

```
CustomerIO.initialize(
  config: CustomerIOConfig(
    cdpApiKey: "YOUR_CDP_API_KEY",
    apiHost: "YOUR_PROXY_HOST",
    cdnHost: "YOUR_CDN_HOST",
    region: Region.us,
    logLevel: CioLogLevel.error
  )
)
```

---

## Getting started > Troubleshooting

**Source:** /integrations/sdk/flutter/getting-started/troubleshooting

# Troubleshooting

If you're having trouble with the SDK, here are some basic steps to troubleshoot your problems, and solutions to some known issues.

## Basic troubleshooting steps

1.  **Make sure your app meets our [prerequisites](/integrations/sdk/flutter/getting-started/#prerequisites)**: Attempting to use our SDK in an environment that doesn't match our supported versions may result in build errors.

1.  **Update to the latest version**: When troubleshooting problems with our SDKs, we generally recommend that you try updating to the latest version. That helps us weed out issues that might have been seen in previous versions of the SDK.
    
2.  **Try running our MCP server**: Our MCP server includes an `integration` tool that can provide immediate help with your implementation, including problems with push and in-app notifications. See [Use our MCP server to troubleshoot your implementation](#troubleshoot-with-mcp) below.
    
3.  **Enable `debug` logging**: Reproducing your issue with `loglevel` set to `debug` can help you (or us) pinpoint problems.
    
    Don't use debug mode in your production app
    
    Debug mode is great for helping you find problems as you integrate with Customer.io, but we strongly recommend that you set `loglevel` to `error` in your publicly available, production app.
    
4.  **Try our test image**: Using [an image](#image-display-issues) that we know works in push and in-app notifications can help you narrow down problems relating to images in your messages.
    

### If you need to contact support

We’re here to help! If you contact us for help with an SDK-related issue, we’ll generally ask for the following information. Having it ready for us can help us solve your problem faster.

1.  **Share information about your device and environment**: Let us know where you had an issue—the SDK and version of the SDK that you’re using, the specific device, operating system, message, use case, and so on. The more information you share with us, the easier it is for us to weed out externalities and find a solution.
    
2.  **Provide comprehensive debug logs**: When sharing logs with our support team, please ensure your logs include:
    
    *   **SDK initialization**: Show that the SDK was initialized with your site ID and API key
    *   **Profile identification**: Show that a profile was identified in your app
    *   **Issue reproduction**: Capture the exact issue you’re experiencing
    *   **Unfiltered logs**: Provide complete, unfiltered logs—don’t remove or filter out any log entries
    *   **Debug level enabled**: Make sure `loglevel` is set to `debug` when capturing logs for support
3.  **For push notification issues**:
    
    *   **Use live push examples**: If your issue relates to push notifications, provide logs from a **live push notification** sent through an automation or API call, not a test send. Live pushes show the actual payload that was delivered to the profile.
    *   **Test in different app states**: Test and document the issue in various app states:
        *   **Foreground**: App is open and active
        *   **Background**: App is running but not in focus
        *   **Killed/Terminated**: App is completely closed
    *   **Include the push payload**: Share the complete push notification payload that you sent.
4.  **[Grant access to your workspace](/accounts/security/privacy/#support-team-access)**: It may help us to see exactly what triggers an automation, what data is associated with devices you’re troubleshooting, etc. You can grant access for a limited time, and revoke access at any time.
    

### Troubleshooting issues with our MCP server

Our [MCP server](/ai/mcp/get-started/) includes an `integration` tool that can help troubleshoot your implementation, including problems with push and in-app notifications. It has a deep understanding of our SDKs and provides an immediate way to get support with your implementation—without necessarily needing to capture debug logs, etc.

You can ask the MCP server basic questions like, “My push notifications aren’t working. Can you help me troubleshoot the problem?”

Or you can ask more specific questions like, “Deep links in push notifications don’t work for customers in my Android app.” Or “I’m not receiving metrics for push notifications for iOS users.”

The tool will return detailed steps to help you find and troubleshoot problems.

### Capture logs

Logs help us pinpoint the problem and find a solution. To capture logs, you should [install Flutter DevTools ↗](https://docs.flutter.dev/tools/devtools) if you haven’t already.

1.  **Enable debug logging in your app**.
    
    Note
    
    You should not use debug mode in your production app. Remember to disable debug logging before you release your app to the App Store.
    
    ```
    import 'package:customer_io/customer_io.dart';
    import 'package:customer_io/customer_io_config.dart';
    import 'package:customer_io/customer_io_enums.dart';
    
    await CustomerIO.initialize(
     config: CustomerIOConfig(
       siteId: "919a7e12107bd03155f6",
       apiKey: "86344654754f1c48d32b",
       region: Region.us,
       //config options go here
       logLevel: CioLogLevel.debug
     ),
    );
    ```
    
2.  Build and run your app on a physical device or emulator.
    
3.  Open the *Logging* view in your development application. If you use *Android Studio*, select *View > Tool Windows > Logcat* to see your logs.
    
4.  Filter for `CIO` in the top to find log messages specific to the Customer.io SDK.
    
5.  Export your log and send it to our Support team at [win@customer.io](mailto:win@customer.io). In your message, describe your problem and provide relevant information about:
    
    *   The version of the SDK you’re using.
    *   The type of problem you’ve encountered.
    *   An existing GitHub issue URL or existing support email so we know what these log files are in reference to.

#### Capturing iOS logs

Logs for iOS are emitted via Apple’s Unified Logging system, so you’ll need to capture them using Xcode or the MacOS console, not just the Flutter debug console. The `logLevel` set in your `CustomerIOConfig` is forwarded to the native iOS SDK, but these logs won’t reliably appear in `flutter run` output.

To capture iOS logs for troubleshooting:

1.  **Run your iOS app via Xcode** or on a device/simulator attached to Xcode.
2.  **Use the macOS Console** to capture logs. You might want ot filter for `CIO` to find log messages specific to the Customer.io SDK.

## NaN, infinite, or imaginary number values

Customer.io doesn’t handle invalid JSON values in your payloads, like `NaN`, infinite, or imaginary number values. If you send these values in `identify`, `track`, `screen`, or similar calls, we’ll drop them and record errors.

While we drop invalid values, we don’t drop the entire payload. The operation itself will still succeed. For example, if you send an identify call with two attributes, one of which is a `NaN` value, we’ll drop the `NaN` value, but the identify call succeeds with the other attribute.

## Push notification issues

### Problems with rich push notifications (images, delivered metrics, etc)

If you have trouble with rich push features, like images not showing up in your push notifications, delivery metrics not being reported when a push notification is visible on the device, and so on, it’s possible that you either need to re-create your NSE target to support rich notifications your you may not have embeded the `NotificationServiceExtension` (NSE) at all.

1.  Remove your current NSE extension.
    
    1.  In XCode, select your project.
    2.  Go to the *Signing & Capabilities* tab.
    3.  Click the NotificationServiceExtension target; it has a bell icon next to it.
    4.  Click the minus sign to remove the target
    5.  Confirm the **Delete** operation.
    
     ![an image illustrating the 5 clicks you'll perform to delete your NSE target.](https://customer.io/images/delete-nse-1.png)
    
2.  Remove existing NSE files.
    
    1.  Right click the `NotificationServiceExtension` folder in your project and select **Delete**.
    2.  Confirm **Move to Trash**.
    
     ![an image illustrating the steps you'll take to delete NSE files.](https://customer.io/images/delete-nse-2.png)
    
3.  Recreate the notification service extension, following instructions for your framework. When You create your target NSE file, make sure you select your app’s name from the *Embed in Application* dropdown.
    
     ![](https://customer.io/images/xcode-servicenotification3.png)
    
4.  Then add the required files:
    
    *   [React Native](/integrations/sdk/react-native/push-notifications/push/#integrate-push-capabilities-in-your-app)
    *   [Flutter](/integrations/sdk/flutter/push-notifications/push-setup/#swift-push)
    *   Expo (does this automatically)
    *   [iOS](/integrations/sdk/ios/push/#rich-push)
5.  After all files are added, go to the NSE target and, under the **General** tab, check **Deployment Target** and set it to a value that is identical to your host app’s iOS version.
    
     ![](https://customer.io/images/troubleshoot-nse.png)
    

When you create a new target, by default, XCode sets the highest version of deployment target version available. While testing if your device’s iOS version is lower than this deployment target, then the NSE won’t be connected to the main target and you won’t receive rich push notifications.

Then you can build and run your app to test if you can receive a rich push notification.

### Why aren’t devices added to profiles in Production builds?

If you see devices register successfully on your Staging builds, but not in Production or TestFlight builds, there might be an issue with your project setup. Check that the Push capability is enabled for both Release and Debug modes in your project. You might also need to enable the *Background Modes (Remote Notifications)* capability, depending on your project setup and messaging needs.

### Image display issues

If you’re having trouble, try using our test image in a message! If it works, then there’s likely a problem with your original image.

 ![a test image of a bird that we know will work with all push notifications](https://customer.io/images/push-image-control.jpg)

Android and iOS devices support different image sizes and formats. In general, you should stick to the smallest size (under 1 MB—the limit for Android devices) and common formats (PNG, JPEG).

iOS

Android

In-App (all platforms)

**Format**

JPEG, PNG, BMP, GIF

JPEG, PNG, BMP

JPEG, PNG, GIF

**Maximum size**

10 MB\*

1 MB

**Maximum resolution**

2048 x 1024 px

1038 x 1038 px

For linked media only. If you host images in our Asset Library, you’re limited to 3MB per image.

### Why didn’t everybody in my segment get a push notification?

If your [segment](/journeys/segmentation/data-driven-segments/) doesn’t specify profiles that have an existing device, it’s likely that profiles entered your segment without using your app. If you send a push notification to such a segment, the “Sent” count will probably show fewer sends than there were profiles in your segment.

### Why are messages sent but not delivered or opened?

The *sent* status means that we sent a message to your delivery provider—APNS or FCM. It’ll be marked *delivered* or *opened* when the delivery provider forwards the message to the device and the SDK reports the metric back to Customer.io. If a profile turned their device off or put it in airplane mode, they won’t receive your push notification until they’re back on a network.

Make sure you've configured your app to track metrics

If your app isn’t set up to capture push metrics, your app will *never* report `delivered` or `opened` metrics!

### Why don’t my messages play sounds?

When you send a push notification to iOS devices that uses our SDK, you can opt to send the *Default* system sound or no sound at all. If your audience’s phone is set to vibrate, or they’ve disabled sound permissions for your app, the *Default* setting will cause the device to vibrate rather than playing a sound.

In most cases, you should use the *Default* sound setting to make sure your audience hears (or feels) your message. But, before you send sound, you should understand:

1.  Your app needs permission from your users to play sounds. This is done by your app, not our SDKs. [Here’s an example from our iOS sample app ↗](https://github.com/customerio/customerio-ios/blob/main/Apps/APN-UIKit/APN%20UIKit/Util/NotificationUtil.swift#L12-L13) showing how to request sound permissions.
2.  iOS users can go into *System Settings* and disable sound permissions for your app. Enabling the *Default* setting doesn’t guarantee that your audience hears a sound when your message is delivered!

We don't support custom sounds yet

If you want to send a custom sound, you’ll need to handle it on your own, outside the SDK and use a custom payload when you set up your push notifications.

### FCM `SENDER_ID_MISMATCH` error

This error occurs when the FCM Sender ID in your app does not match the Sender ID in your Firebase project. To resolve this issue, you’ll need to ensure that the Sender ID in your app matches the Sender ID in your Firebase project.

1.  Check that you uploaded the correct JSON certificate to Customer.io. If your JSON certificate represents the wrong Firebase project, you may see this error.
2.  Verify that the Sender ID in your app matches the Sender ID in your Firebase project.
3.  If you imported devices (device tokens) from a previous project, make sure that you imported tokens from the correct Firebase project. If the tokens represent a different app than the one you send push notifications to, you’ll see this error.

In some cases, we may make fixes in our iOS push packages that fix downstream issues in the Flutter SDK. Before you contact support, you might want to [update your iOS dependencies](/integrations/sdk/flutter/push-notifications/push-setup/) to get the latest packages and see if that fixes the issue.

You can also check out our latest iOS changes to see if we've already fixed the issue or check out open issues to see if you're experiencing a known issue.

### Deep links on iOS only open in a browser

It sounds like you want to use [universal links](/universal-links/#deep-links-vs-universal-links), links that go to your app if a person has your app installed and to your website if they don't. Universal links are a bit different than your average deep link and require a little bit of additional setup.

### Notifications not coming through when app is in background

If your app does not receive push notifications when it’s in the background, check the following:

1.  Ensure that you have implemented the `_firebaseMessagingBackgroundHandler` as suggested by the [Firebase Messaging documentation ↗](https://firebase.google.com/docs/cloud-messaging/flutter/receive#background_messages). This handler is responsible for processing messages received while the app is in the background or closed.
2.  Verify that the handler is set correctly and receiving callbacks. Double-check the implementation to ensure it’s properly registered within your app’s code.
3.  Confirm the Flutter version you are using. For Flutter version 3.3.0 or higher, you will need to add `@pragma('vm:entry-point')` to the handler function for it to work correctly.

## Swift Package Manager (SPM) issues

### `Unable to find module dependency: 'CioMessagingPushFCM'` in NSE

The NotificationServiceExtension target is missing the SPM package link.

1.  In Xcode, select the **NotificationServiceExtension** target and go to **General** > **Frameworks and Libraries**
2.  Click **+** and add **FlutterGeneratedPluginSwiftPackage**.

 ![Add FlutterGeneratedPluginSwiftPackage to the NotificationServiceExtension target](https://customer.io/images/xcode-nse-spm-framework.png)

### `redefinition of module 'Firebase'`

Firebase is being loaded from both CocoaPods and SPM, causing a conflict.

1.  Update your Firebase dependencies to SPM-compatible versions in your `pubspec.yaml`:
    
    ```
    firebase_core: ^3.5.0  # we recommend at least ^4.6.0
    firebase_messaging: ^15.2.0  # we recommend at least ^16.1.3
    ```
    
2.  Remove any manual Firebase pod lines from your `ios/Podfile` and run `flutter pub upgrade firebase_core firebase_messaging`.
    

### `requires minimum platform version 15.0 for the iOS platform`

The Customer.io SDK and Firebase require iOS 15.0 or later.

Set the deployment target to 15.0 or later in Xcode:

1.  Select the **Runner** project and go to **Build Settings**
2.  Set **iOS Deployment Target** to **15.0**.
3.  Update your `ios/Podfile`:
    
    ```
    platform :ios, '15.0'
    ```
    

### NSE unable to find `CioMessagingPushFCM` dependency

The NotificationServiceExtension can’t resolve the Customer.io SDK dependency. This can happen in both local builds and CI environments.

1.  Make sure SPM is enabled before building. You can enable it globally or per-project:
    
    *   **Global**: Run `flutter config --enable-swift-package-manager`
    *   **Per-project**: Add `enable-swift-package-manager: true` to the `flutter:` > `config:` section of your `pubspec.yaml`
2.  Clean and rebuild:
    
    ```
    flutter clean
    flutter pub get
    flutter build ios
    ```
    

In your CI pipeline, make sure that you run `flutter config --enable-swift-package-manager` before running `flutter pub get` to ensure that the configuration takes effect.

### `import UserNotifications` error

SPM doesn’t transitively import system frameworks the way CocoaPods does. If your NSE fails to compile with errors about `UNNotificationServiceExtension`, `UNNotificationRequest`, or `UNNotificationContent`, you’re missing the system import.

Add `import UserNotifications` at the top of your `NotificationService.swift`:

```
import CioMessagingPushFCM
import UserNotifications
```

## In-App message issues

### My in-app messages are sent but not delivered

Profiles won’t get your message until they open your app. If you use page rules, they won’t see your message until they visit the right screen(s), so delivery times for in-app messages can vary significantly from other types of messages.

---

## In app messages > In app

**Source:** /integrations/sdk/flutter/in-app-messages/in-app

# In-app messages

## How it works

An in-app message is a message that people see in your app. People won’t see your in-app messages until they open your app. If you set an *expiry* period for your message, and that time elapses before someone opens your app, they won’t see your message.

You can also set *page rules* to display your in-app messages when people visit specific pages in your app. However, to take advantage of page rules, you need to use [screen tracking](/integrations/sdk/flutter/tracking/screen-events/#auto-screenview) features. Screen tracking tells us the titles of your screens and which screen a person is on, so we can display in-app messages on the correct pages/screens in your app.

## Set up in-app messaging

In-app messages are disabled by default. Just pass the `inAppConfig` parameter in your `CustomerIOConfig` and add your [Site ID](https://fly.customer.io/env/last/settings/api_credentials).

```
import 'package:customer_io/customer_io.dart';
import 'package:customer_io/customer_io_config.dart';
import 'package:customer_io/customer_io_enums.dart';

CustomerIO.initialize(
  config: CustomerIOConfig(
    cdpApiKey: 'cdpApiKey',
    region: Region.us,
    inAppConfig: InAppConfig(siteId: 'siteId')
  ),
);
```

## Page rules

When you send an in-app message, you can set *Page Rules* determining the page(s) where your audience can see your message. Before you can take advantage of page rules, you need to:

1.  Track screens in your app. See [Screen Events](/integrations/sdk/flutter/tracking/screen-events/) for help sending `screen` events.
2.  Provide screen titles to whoever sets up in-app messages in Customer.io. If we don’t recognize the page that you set for a page rule, your audience will never see your message!

 ![Set up page rules to limit in app messages by page](https://customer.io/images/in-app-page-rule.png)

Keep in mind: page rules are case sensitive. If you’re targeting your mobile app, make sure your page rules match the casing of the `title` in your `screen` events. If you’re targeting your website, your page rules should always be lowercase.

 ![The first page rule is Web contains /dashboard. The second page rule is iOS contains Dashboard.](https://customer.io/images/page-rule-case-sensitive.png)

---

## In app messages > In app actions

**Source:** /integrations/sdk/flutter/in-app-messages/in-app-actions

# In-app event listeners

## How it works

In-app messages often have a call to action. Most basic actions are handled automatically by the SDK. For example, if you set a call-to-action button to open a web page, the SDK will open the web page when the user taps the button.

But you can also set up custom actions that require your app to handle the response. If you set up custom actions, you’ll need to handle the action yourself and dismiss the resulting message when you’re done with it.

### Handle responses to messages (event listeners)

You can set up event listeners to handle your audience’s response to your messages. For example, you might run different code in your app when your audience taps a button in your message or when they dismiss the message without tapping a button.

You can listen for four different events:

*   `messageShown`: a message is “sent” and appears to a user
*   `messageDismissed`: the user closes the message (by tapping an element that uses the `close` action)
*   `errorWithMessage`: the message itself produces an error—this probably prevents the message from appearing to the user
*   `messageActionTaken`: the user performs an action in the message.

After you initialize the SDK, you can register an event listener to subscribe to in-app events. In the code below, `event` is an instance of `InAppMessageEvent` containing details about the in-app message, e.g. `messageId`, `deliveryId`.

```
// subscribe to stream
StreamSubscription subscription = CustomerIO.inAppMessaging.subscribeToEventsListener((InAppEvent event) {
  // cases for each event.eventType
  switch (event.eventType) {
    case EventType.messageShown:
      print("messageShown: ${event.message}");
      break;
    case EventType.messageDismissed:
      print("messageDismissed: ${event.message}");
      break;
    case EventType.errorWithMessage:
      print("errorWithMessage: ${event.message}");
      break;
    case EventType.messageActionTaken:
      // event.actionValue => The type of action that triggered the event.
      // event.actionName => The name of the action specified when building the in-app message.
      print("messageActionTaken: ${event.message}");
      break;
  }
});

// to unsubscribe from the event listener
subscription.cancel();
```

## Handling custom actions

When you set up an in-app message, you can determine the “action” to take when someone taps a button, taps your message, etc. In most cases, you’ll want to deep link to a screen, etc. But, in some cases, you might want to execute some custom action or code—like requesting that a user opts into push notifications or enables a particular setting.

In these cases, you’ll want to use the `messageActionTaken` event listener and listen for custom action names or values to execute code. While you’ll have to write custom code to handle custom actions, the SDK helps you [listen for in-app message events](#event-listeners) including your custom action, so you know when to execute your custom code.

1.  When you [add an action to an in-app message](/messaging/channels/in-app/send-in-app-message/) in Customer.io, select *Custom Action* and set your Action’s *Name* and value. The *Name* corresponds to the `actionName`, and the value represents the `actionValue` in your event listener.
    
     ![Set up a custom in-app action](https://customer.io/images/in-app-custom-action.png)
    
2.  [Register an event listener](#event-listener) for `MessageActionTaken`, and listen for the `actionName` or `actionValue` you set up in the previous step.
    
    Use names and values exactly as entered
    
    We don’t modify your action’s name or value, so you’ll need to match the case of names or values exactly as entered in your *Custom Action*.
    
3.  When someone receives a message and invokes the action (tapping a button, tapping a message, etc), your app will perform the custom action.
    

## Dismiss in-app message

You can dismiss the currently display in-app message with the following method. This can be particularly useful to dismiss in-app messages when your audience clicks or taps custom actions.

```
CustomerIO.inAppMessaging.dismissMessage();
```

## Deep links

You can open deep links when a user clicks actions inside in-app messages. Setting up deep links for in-app messages is the same as [setting up deep links for push notifications](/integrations/sdk/flutter/push-notifications/deep-links/).

---

## In app messages > Inbox

**Source:** /integrations/sdk/flutter/in-app-messages/inbox

# Notification inbox

## How it works

Unlike other messages, inbox messages don’t necessarily appear immediately to your audience, and they don’t disappear when a person dismisses them. Instead, messages collect in a notification inbox that people can check at their leisure. You send inbox messages as part of an [automation, broadcast](/messaging/channels/in-app/inbox/send-inbox/), or [transactional message](/messaging/channels/in-app/inbox/send-inbox-txnl/).

The SDK gives you two ways to show the inbox in your app:

*   **Recommended: [add the visual inbox](#add-the-visual-inbox).** Mount the SDK’s ready-made inbox widgets—a bell icon with an unread badge and a panel that lists messages. The SDK renders the [inbox you style and publish](/messaging/channels/in-app/inbox/get-started/) in Customer.io using native UI—no webviews—and handles fetching messages, unread counts, opens, and clicks for you.
*   **Advanced: [build your own inbox](#build-your-own-inbox).** The SDK delivers inbox messages as JSON payloads, and you render them in your own inbox UI. Take this path when you need complete control over the inbox experience.

## Add the visual inbox

The visual inbox renders the same inbox you publish for your website: your icon, colors, position, unread indicator, and message templates—including dark mode styles. When there’s nothing to show—the inbox isn’t published or has no messages—the inbox widgets render nothing, so they’re safe to mount unconditionally.

### Prerequisites

*   [Create, style, and publish a notification inbox](/messaging/channels/in-app/inbox/get-started/) in your workspace.
*   [Set up in-app messaging](/integrations/sdk/flutter/in-app-messages/in-app/) in your app. The inbox widgets build on the in-app messaging module.
*   Update to the latest version of the Flutter SDK. The visual inbox ships with the SDK—there’s no separate module to install.

### Add the inbox to your app

The visual inbox widgets come from `package:customer_io/customer_io_widgets.dart`. `NotificationInboxOverlay` is the all-in-one option: a floating bell button with an unread badge that slides out the message list, with a scrim that closes it when a person taps outside. Mount it once, near the root of your widget tree—for example, in a `Stack`—so it sits on top of your content.

```
import 'package:customer_io/customer_io_widgets.dart';
import 'package:flutter/material.dart';

class RootView extends StatelessWidget {
  const RootView({super.key});

  @override
  Widget build(BuildContext context) {
    return const Stack(
      children: [
        AppContent(),
        NotificationInboxOverlay(),
      ],
    );
  }
}
```

That’s the whole integration. The overlay fetches messages, shows and updates the unread badge, marks messages opened when the panel opens, and reports opens and clicks to your [inbox metrics](/messaging/channels/in-app/inbox/metrics/).

### Place the bell and message list yourself

If the floating overlay doesn’t fit your design, compose the two underlying widgets directly:

*   `NotificationInboxBell`: the bell button with the unread badge. Place it anywhere—an app bar, a tab, a corner—and use its `onTap` callback to present your inbox. The bell renders nothing when there’s nothing to show, so give it bounded constraints (for example, wrap it in a `SizedBox`) for predictable layout.
*   `NotificationInboxView`: the message list without any surrounding chrome. Embed it in a sheet, a tab, or a dedicated inbox screen. It needs a bounded height—wrap it in an `Expanded` or a sized container.

```
import 'package:customer_io/customer_io_widgets.dart';
import 'package:flutter/material.dart';

class DashboardView extends StatelessWidget {
  const DashboardView({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Dashboard'),
        actions: [
          SizedBox(
            width: 56,
            height: 56,
            child: NotificationInboxBell(
              onTap: () => _showInbox(context),
            ),
          ),
        ],
      ),
      body: const AppContent(),
    );
  }

  void _showInbox(BuildContext context) {
    showModalBottomSheet<void>(
      context: context,
      builder: (_) => const SizedBox(
        height: 500,
        child: NotificationInboxView(),
      ),
    );
  }
}
```

### Handle message actions

When a person taps a message or a button inside it, the SDK runs the action you set on the message: it opens URLs, removes the message for dismiss actions, and so on. If your messages use deep links or you want to route navigation through your own code, set an `InboxEventListener` on the in-app messaging module.

```
import 'package:customer_io/customer_io.dart';
import 'package:customer_io/messaging_in_app.dart';

class InboxListener extends InboxEventListener {
  @override
  void messageActionTaken(InboxMessage message, String actionName, String actionValue) {
    // Navigate within your app
    if (actionValue.startsWith('myapp://')) {
      DeepLinkRouter.open(actionValue);
    }
  }

  @override
  void messageShown(InboxMessage message) {}

  @override
  void messageOpened(InboxMessage message) {}

  @override
  void messageDismissed(InboxMessage message) {}
}

CustomerIO.inAppMessaging.setInboxEventListener(InboxListener());
```

Note

While an `InboxEventListener` is registered, your app owns action navigation: the SDK stops opening URLs and deep links itself for tapped actions and forwards them to your listener instead. Pass `null` to `setInboxEventListener` to restore the SDK’s default behavior. The SDK always handles dismiss actions—they aren’t forwarded to your listener.

The listener’s other callbacks are observe-only—use them for analytics or app state:

Callback

When it fires

`messageActionTaken(message, actionName, actionValue)`

A person taps a non-dismiss action.

`messageShown(message)`

A message renders in the visible inbox for the first time.

`messageOpened(message)`

The SDK marks a message opened—for example, when the panel opens.

`messageDismissed(message)`

A person dismisses a message, removing it from the inbox.

### Visual inbox behaviors

*   **The bell hides itself when there’s nothing to show.** If your inbox isn’t published, or the person has no messages, the bell and panel don’t render at all.
*   **Opening the inbox marks messages opened.** Messages visible in the open panel—or in an embedded `NotificationInboxView`—are marked opened automatically, and the unread badge updates to match.
*   **Dismissing a message deletes it.** When a person dismisses a message, the SDK removes it from their inbox on every device.
*   **Dark mode is automatic.** The inbox follows the device’s appearance and re-renders when it changes, using the dark mode colors you set in Design Studio.
*   **Style changes flow to your app.** When you update your inbox’s styles or templates in Customer.io, the SDK picks up the changes on its next refresh—no app update required.

## Build your own inbox

If you need complete control over the inbox experience, skip the visual inbox and work with the message payloads directly. Customer.io delivers inbox messages as JSON payloads, not fully rendered messages. The SDK helps you listen for these payloads, but you determine how to display them in your own inbox client.

## Get the inbox instance

You’ll access inbox functionality through the `inbox` property on the in-app messaging module.

```
final inbox = CustomerIO.inAppMessaging.inbox;
```

## Inbox methods

The inbox instance provides several methods to manage messages.

Method

Description

`fetchMessages({String? topic})`

Fetch messages from the inbox. Optionally filter by topic. Returns a Future with the list of messages.

`messages({String? topic})`

Stream messages from the inbox for real-time updates. Optionally filter by topic. Returns a Stream of message lists.

`markMessageOpened(message)`

Mark a message as opened.

`markMessageUnopened(message)`

Mark a message as unopened.

`markMessageDeleted(message)`

Mark a message as deleted.

`trackMessageClicked(message, {String? actionName})`

Track a click on the message. The `actionName` parameter is optional.

## Inbox message payloads

Inbox messages are delivered as a JSON payload. The SDK helps you listen for the payload, but you’ll render the content in your own inbox client.

The client payload includes the following fields, but you’re most concerned with the `properties` object, which represents your message content. By default, we’ll send a `title` and `body` field, but you can add other fields like an `image` or a `link`—whatever you set up your inbox to expect.

Make sure that your team members know what payloads to send—especially if you expect different payloads for different topics or types of messages.

Field

Type

Description

`messageId`

string

Unique identifier for the message.

`sentAt`

string

When the message was sent.

`expiresAt`

string

When the message will expire.

`opened`

boolean

Whether the message has been opened.

`topics`

array

The topics that the message belongs to.

`type`

string

The type of message.

`properties`

object

The properties of the message.

```
{
    "messageId": "1234567890",
    "sentAt": "2026-02-05T12:00:00Z",
    "expiresAt": "2026-02-05T12:00:00Z",
    "opened": false,
    "topics": ["orders", "shipping"],
    "type": "order_shipped",
    "properties": {
        "title": "Hey Cool Person, your order shipped!",
        "body": "You can track your order #1234567890 here:",
        "link": "https://example.com/orders/1234567890"
    }
}
```

### Inbox topics and types

When you send an inbox message, you can assign it to one or more topics. You can use these topics to filter messages when you fetch them. You can also use the topics to determine how to render the messages in your notification inbox.

Messages also have a `type`. Think of this like a sub-category or topic for a message. For example, you might have `orders` and `sale` topics, where `orders` don’t have images but `sale` topics might. Or, within the `orders` topic, you might have `order_placed` and `order_shipped` types, where `order_placed` lists order details and images of purchased products and `order_shipped` provides a link to the tracking information for the order that opens in a new tab.

## Setup your notification inbox

Inbox messages are just JSON payloads. You’ll need to build your own inbox client to display the messages. The code below gives you a starting point, but you can build your own inbox client however you want.

### Fetch messages

```
// Fetch all messages
final messages = await inbox.fetchMessages();

// Fetch messages filtered by topic
final promotions = await inbox.fetchMessages(topic: 'promotions');
```

### Stream messages for real-time updates

```
// Stream all messages (real-time updates)
inbox.messages().listen((messages) {
  // Update your UI with the messages
  updateInboxUI(messages);
});

// Stream messages filtered by topic
inbox.messages(topic: 'promotions').listen((messages) {
  // Update your UI with promotions
  updatePromotionsUI(messages);
});
```

### Mark messages as opened or unopened

```
// Mark a message as opened
inbox.markMessageOpened(message);

// Mark a message as unopened
inbox.markMessageUnopened(message);
```

### Track message clicks

```
// Track a click without an action name
inbox.trackMessageClicked(message);

// Track a click with an action name
inbox.trackMessageClicked(message, actionName: 'view_details');
```

### Delete messages

```
// Mark a message as deleted
inbox.markMessageDeleted(message);
```

## Working with message properties

You can access message properties to display custom content in your inbox:

```
// Access message properties
final title = message.properties['title'] as String?;
final body = message.properties['body'] as String?;
final link = message.properties['link'] as String?;
final imageUrl = message.properties['image'] as String?;

// Handle message action when user taps
void handleMessageTap(InboxMessage message) {
  // Mark as opened
  inbox.markMessageOpened(message);

  // Track the click
  inbox.trackMessageClicked(message);

  // Open link if available
  final link = message.properties['link'] as String?;
  if (link != null) {
    launchUrl(Uri.parse(link));
  }
}
```

---

## In app messages > Inline in app

**Source:** /integrations/sdk/flutter/in-app-messages/inline-in-app

# Inline in-app messages

## How it works

An inline message targets a specific widget in your app. You create a placeholder `InlineInAppMessageView` in your UI, and the SDK fills it with the content of your message.

Inline messages let you show dynamic content without releasing a new version of your app. Unlike push notifications, banners, or modal in-app messages, an inline message looks and feels like part of your interface.

## 1\. Add View to your app UI to support inline messages

Add `InlineInAppMessageView` anywhere you want to display inline messages. The widget expands or contracts automatically when a message loads or when people interact with it.

Tip

See [our sample apps ↗](https://github.com/customerio/customerio-flutter/tree/main/apps) for real-world implementations.

```
import 'package:customer_io/customer_io.dart';
import 'package:customer_io/customer_io_widgets.dart';

class InlineExample extends StatelessWidget {
  const InlineExample({super.key});

  @override
  Widget build(BuildContext context) {
    return InlineInAppMessageView(
      elementId: 'inline', // Use this ID in Customer.io when you build your message.
      onActionClick: (
        InAppMessage message,
        String actionValue,
        String actionName,
      ) {
        // Handle button taps or other actions.
        debugPrint(
          'Inline message action clicked: $actionName with value: $actionValue',
        );
      },
    );
  }
}
```

### View layout

*   Avoid hard-coding a height. The widget manages its own height as messages load and change.
*   You control layout—padding, margins, alignment—just like any other widget.

## 2\. Build and send your message

When you add an in-app message to a broadcast or automation in Customer.io:

1.  Set **Display** to **Inline**.
2.  Enter the **Element ID** that matches the `elementId` you set in your widget.
3.  (Optional) If you send multiple messages to the same Element ID, set a **Priority** so we know which message to show first.

Then design and send your message!

 ![Inline display settings in the in-app message editor](https://customer.io/images/in-app-inline.png)

## Handling custom actions

When you configure an in-app message, you decide what happens when someone taps a button or the message itself. For deep links, the SDK opens the link automatically. For other interactions—like showing a settings page—you can listen for action events and run your own code.

### 1\. Compose a message with a custom action

In Customer.io, add an action to your in-app message, choose **Custom Action**, and set the action *Name* and *Value*. The *Name* maps to `actionName`; the *Value* maps to `actionValue` in your callback.

 ![Set up a custom in-app action](https://customer.io/images/in-app-custom-action.png)

### 2\. Listen for events

You have two ways to detect clicks in inline messages.

1.  **Callback on the widget** – Pass `onActionClick` (shown above) to handle actions for that specific inline view.
2.  **Global listener** – Subscribe to `CustomerIO.inAppMessaging.subscribeToEventsListener` to handle inline **and** modal message events in one place.

```
final subscription = CustomerIO.inAppMessaging.subscribeToEventsListener(
  (InAppEvent event) {
    if (event.eventType == EventType.messageActionTaken) {
      // Perform your logic here.
      debugPrint('Action taken: ${event.actionName} / ${event.actionValue}');
    }
  },
);

// Later, when you no longer need events
subscription.cancel();
```

## Handle responses to messages (event listeners)

Just like modal messages, inline messages emit events you can react to:

*   `messageShown` – The message appeared.
*   `errorWithMessage` – We encountered an error rendering the message.
*   `messageActionTaken` – Someone tapped an action. This only triggers if the inline view does **not** have an `onActionClick` callback.

Inline messages have no *dismiss* concept, so there is no `messageDismissed` event.

---

## Live notifications > Reference

**Source:** /integrations/sdk/flutter/live-notifications/reference

# Live notification payload reference

Each payload constructor selects a template. `start` and `update` take the same shape—pass the complete state every time, because each update replaces the previous one rather than patching it.

`start` returns the notification’s ID. Keep it: `update` and `end` both take it, and it’s what you hand your server if you want it to take over.

The field names match the ones your server sends through the API. The plugin flattens them into a single payload: `header` is a static attribute, and everything else is content state.

## Multi-step tracker

```
final activityId = await CustomerIO.liveActivities.start(
  LiveActivityPayload.segments(
    header: "Chica's Tacos",
    status: 'Preparing your order',
    substatus: "We'll let you know when it's on the way",
    segmentsTotal: 4,
    segmentsComplete: 2,
    trailingText: '25 min',
  ),
);
```

Field

Type

Required

Description

`header`

`String`

Yes

Static header line. Set at start; can’t change.

`status`

`String`

Yes

Primary status line.

`substatus`

`String?`

No

Secondary status line.

`segmentsTotal`

`int`

Yes

Total number of steps. Capped at 20 steps.

`segmentsComplete`

`int`

Yes

Steps finished so far. Clamped to `0`–`segmentsTotal`.

`trailingText`

`String?`

No

Trailing text, such as an ETA. Typically matters for iOS. Android has a single slot for the message body; when both `substatus` and `trailingText` are set, the message body renders `substatus` and drops `trailingText`.

## Countdown timer

```
final activityId = await CustomerIO.liveActivities.start(
  LiveActivityPayload.countdownTimer(
    header: 'Summer Sale',
    title: 'Flash sale ends in',
    statusMessage: '30% off everything',
    endTime: 1753603600,
  ),
);
```

Field

Type

Required

Description

`header`

`String`

Yes

Static header line. Set at start; can’t change.

`title`

`String`

Yes

Primary title line.

`statusMessage`

`String?`

No

Status message below the title.

`endTime`

`int?`

No

Target time in **epoch seconds**, not milliseconds. Omit it to render the finished state.

## Custom

A custom notification has no schema—you render it, so the SDK passes your values through untouched.

```
final activityId = await CustomerIO.liveActivities.start(
  LiveActivityPayload.custom(
    data: {
      'driver': 'Sam',
      'vehicle': 'Blue Prius',
      'minutesAway': '4',
    },
  ),
);
```

Every value in `data` must be a string, and it carries the complete state on every update. Nested objects and arrays aren’t supported, and the platforms disagree on them—iOS drops them and Android turns them into strings. Flatten anything structured before you send it.

---

## Live notifications > Set up

**Source:** /integrations/sdk/flutter/live-notifications/set-up

# Set up live notifications

## How it works

Live notifications show information that changes over time in places your customers already look: the iOS Lock Screen and the Android notification panel. On iOS they’re built on Apple’s Live Activities; on Android, they’re built on Google’s Live Updates.

The Flutter SDK includes two built-in templates that both native SDKs render for you: a multi-step tracker and a countdown timer. You don’t need to write a platform-specific UI for either one. You can also define a single custom type and render it yourself.

Four actors drive every live notification:

*   **You** enable the notification types your app uses, and start notifications from Dart when something happens in your app.
*   **The SDK** registers the push tokens each notification needs. It also reports to Customer.io when your app starts or ends a notification, and when a customer dismisses one. Updates your app makes on the device aren’t reported.
*   **Your server** starts, updates, and ends notifications by calling [the API](/integrations/api/app/tag/live-notifications/). It never talks to APNs or FCM directly.
*   **Customer.io** turns each API call into a push, using the tokens the SDK registered, and records every start, update, and end as a delivery.

For the concepts behind the feature and the operations your server calls, see the [live notifications overview](/messaging/channels/live-notifications/overview/).

## Prerequisites

Live notifications use the push credentials your app already has in Customer.io. Before you begin:

*   Set up [push notifications](/integrations/sdk/flutter/push-notifications/push-setup/) for your app.
*   You must identify your app user before you can start a notification. The SDK holds token registrations until you identify a person, and [the API](/integrations/api/app/tag/live-notifications/startLiveNotification/) rejects requests that target anonymous profiles.
*   On iOS, target **iOS 16.2 or later**. Starting a notification from your server (push-to-start) requires iOS 17.2.
*   On Android, request `POST_NOTIFICATIONS` on Android 13 and later. To get Android 16’s promoted **Live Updates** treatment, also declare `POST_PROMOTED_NOTIFICATIONS` in your manifest. Without it, live notifications still render as ongoing notifications.

## Set up your Android project

You don’t need to do any build-time setup on Android. Live notifications are part of the push module, so they’re available as soon as you [configure them in your `CustomerIOConfig`](#initialize-the-sdk-with-live-notifications).

## Set up your iOS project

To support iOS, you need to enable the Live Activities module, add a widget extension to render your notifications, and forward widget URLs to the SDK so taps are attributed.

### Enable the Live Activities module

Live notifications are opt-in on iOS.

CocoaPodsSPM

#### CocoaPods

Add the `liveactivities` subspec to your Podfile. Open `ios/Podfile` in your Flutter project and add the following line alongside your existing `customer_io` pod:

```
pod 'customer_io/liveactivities', :path => '.symlinks/plugins/customer_io/ios'
```

#### SPM

Add the following property to your project’s `android/gradle.properties` file. Despite the location, this flag controls the iOS build too:

```
customerio_live_activities_enabled=true
```

If the flag isn’t detected (for example, in Flutter add-to-app modules or some CI environments), set the `CIO_LIVE_ACTIVITIES` environment variable instead:

```
CIO_LIVE_ACTIVITIES=true flutter build ios
```

Then set `NSSupportsLiveActivities` in `ios/Runner/Info.plist`. Without it, iOS refuses to start any notification, so nothing appears even when everything else is configured:

```
<key>NSSupportsLiveActivities</key>
<true/>
```

### Add a widget extension

iOS renders live notifications from a widget extension, so your app needs one even when you only use the built-in templates. In Xcode, choose **File > New > Target > Widget Extension**.

A widget extension can’t link the Flutter plugin pod, so name the rendering pods in its own Podfile target. Keep the version in step with the native iOS SDK version the plugin pins, which you’ll find as `native_sdk_version` in the plugin’s `pubspec.yaml`:

```
target 'LiveActivityWidget' do
  use_frameworks!
  pod 'CustomerIOLiveActivitiesTemplates', '4.7.2'
  pod 'CustomerIOLiveActivitiesAttributes', '4.7.2'
end
```

That pin is the native version the wrapper ships with today. Read yours before you copy it—an extension built against a different native SDK than your app can fail to link.

Then add the SDK’s built-in widgets to your extension’s `WidgetBundle`. Both ship in `CioLiveActivities_Templates`:

```
import CioLiveActivities_Templates
import SwiftUI
import WidgetKit

@main
struct LiveActivityWidgetBundle: WidgetBundle {
  var body: some Widget {
    CIOSegmentsLiveActivity()
    CIOCountdownTimerLiveActivity()
  }
}
```

List only the widgets for the templates you enabled.

### Forward widget URLs to the SDK

When someone taps a live notification, iOS opens your app with the widget URL. Forward it to the SDK from your `AppDelegate` to attribute the tap to the notification:

```
import customer_io

override func application(_ app: UIApplication, open url: URL,
                          options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
  guard let routableUrl = CustomerIOLiveActivities.handleWidgetUrl(url) else { return true }
  return super.application(app, open: routableUrl, options: options)
}
```

`handleWidgetUrl` reports the `opened` metric and returns the URL to route to. A URL that didn’t come from Customer.io comes back unchanged. `nil` means the notification did not contain a deep link, so there’s nothing to open. Android needs no equivalent step.

#### Under UIScene (iOS 27)

The `AppDelegate` example above is the no-change path for AppDelegate-only hosts. If your app adopts iOS’s `UIScene` lifecycle, Customer.io handles scene-delivered Live Activity taps for you. You don’t forward widget URLs from a scene delegate yourself. If an existing scene integration added `handleWidgetUrl` there manually, remove that call before upgrading to avoid reporting the same tap twice.

Scene attribution for Live Activities requires **both**:

*   the opt-in Live Activities module (this page), and
*   a Flutter registrar that supports scene delegates, which needs **Flutter 3.44.8 or newer** and the standard `UIApplicationSceneManifest` with Flutter deep linking enabled. See [Set up push notifications](/integrations/sdk/flutter/push-notifications/push-setup/#uiscene-migration) for the migration steps.

With those in place, Customer.io unwraps and attributes the Live Activity tap once, then offers the destination to Flutter’s router. Ordinary links stay owned by Flutter, and `http`/`https` destinations reach your Dart router so your host owns final navigation. Keep the `AppDelegate` `handleWidgetUrl` example above only for legacy AppDelegate-only hosts.

For the full walkthrough, see the [iOS 27 UIScene migration guide](/integrations/sdk/ios/whats-new/ios-27-uiscene-migration/). Only a single simultaneous window scene is supported; multi-window is out of scope for this release.

## Initialize the SDK with live notifications

Add a `liveNotificationsConfig` to your `CustomerIOConfig`, listing the built-in types your app uses:

```
CustomerIO.initialize(
  config: CustomerIOConfig(
    cdpApiKey: 'your-cdp-api-key',
    liveNotificationsConfig: LiveActivitiesConfig(
      types: [
        LiveActivityTemplate.segments,
        LiveActivityTemplate.countdownTimer,
      ],
    ),
  ),
);
```

Enabling a type registers it for push-to-start, so your server can create that notification remotely. Unrecognized identifiers are ignored, so a template added in a newer native SDK won’t break an older build of your app.

Option

Type

Default

Description

`types`

`List<LiveActivityTemplate>`

`[]`

Built-in templates to enable. Don’t list `custom` here—`customType` enables it.

`customType`

`String?`

`null`

Reverse-DNS identifier for your own type, such as `com.myapp.rideshare`.

`branding`

`LiveActivitiesBranding?`

`null`

Styling for the built-in templates. Android only.

### Branding on Android

Android renders Customer.io’s built-in templates itself, so you can pass a `branding` object to customize their appearance. iOS ignores it, because your widget extension’s SwiftUI defines the appearance there.

```
liveNotificationsConfig: LiveActivitiesConfig(
  types: [LiveActivityTemplate.segments],
  branding: LiveActivitiesBranding(
    accentColorHex: '#1B5E20',
    logoResource: 'brand_logo',
    smallIconResource: 'ic_notification',
  ),
),
```

`logoResource` and `smallIconResource` name bundled Android drawable resources. You can use `logoUrl` for a remote logo instead, but a bundled resource renders on the first frame without a network round-trip.

## Render a custom type

To send data that Customer.io’s built-in templates don’t support by default, you can name your own type with `customType` and render it on both platforms yourself:

```
liveNotificationsConfig: LiveActivitiesConfig(
  types: [LiveActivityTemplate.segments],
  customType: 'com.myapp.rideshare',
),
```

Then start it with `LiveActivityPayload.custom`. On each platform, you need to render the notification:

*   **iOS**: render the SDK’s `CIOCustomAttributes` in your widget extension with your own SwiftUI.
*   **Android**: implement `createLiveNotification` in a `CustomerIOLiveNotificationsCallback` and register it with `CustomerIOLiveActivities.setLiveNotificationCallback`. Register it in `Application.onCreate`, before `CustomerIO.initialize` runs—the native SDK only accepts the callback at build time, and a push can start your process with no Activity. Your callback receives the same `data` map as the one you pass to `start`.

---

## Live notifications > Start

**Source:** /integrations/sdk/flutter/live-notifications/start

# Start live notifications

Your server can drive a notification’s whole lifecycle through [the API](/integrations/api/app/tag/live-notifications/), which works whether or not your app is running. To start one from your app instead—the moment someone places an order, for example—use `CustomerIO.liveActivities`:

```
final activityId = await CustomerIO.liveActivities.start(
  LiveActivityPayload.segments(
    header: "Chica's Tacos",
    status: 'Order received',
    segmentsTotal: 4,
    segmentsComplete: 1,
  ),
);

await CustomerIO.liveActivities.update(
  activityId,
  LiveActivityPayload.segments(
    header: "Chica's Tacos",
    status: 'Out for delivery',
    segmentsTotal: 4,
    segmentsComplete: 3,
  ),
);

await CustomerIO.liveActivities.end(activityId);
```

`start` returns the notification’s ID and registers it with Customer.io, so your server can take over updates from there. Local starts and ends are reported to Customer.io; local updates change the notification on the device without appearing in the delivery timeline.

`end` also accepts a final payload—`end(activityId, payload: ...)`—which iOS renders as the closing state. See [Platform differences](#platform-differences) for how each platform handles it.

There’s no lifecycle event stream in Dart, because neither native SDK exposes one.

## Platform differences

One wrapper API drives both platforms, but some behavior differs underneath. Plan for these differences before you design your flow.

Behavior

iOS

Android

What to do

Updating a notification your app didn’t start

Failure. The SDK needs a process-local handle, so it can only update notifications your app started in the current session.

Success. The ID goes straight to the native SDK.

Don’t update a notification your app didn’t start. Instead, drive those updates from your server, which works on both platforms and doesn’t depend on your app running.

Final content on end

Renders the payload you pass to `end` as the closing state. Without one, the last state stays on screen.

Ignores the payload and renders its own terminal state.

Pass a final payload to `end` when you want a specific closing state on iOS. Don’t count on Android showing it.

Branding

Ignored at runtime—your widget extension’s SwiftUI defines the appearance. On Expo, the plugin compiles your `branding` block into the widget it generates for you.

Applied to every built-in template.

Set `branding` for Android. Style iOS in your widget extension’s SwiftUI, or in the Expo plugin’s `branding` block.

Static fields after start

Fixed. `header` is an attribute, so updates can’t change it.

Fixed by convention only. The device re-renders from each push, so an update that omits `header` drops it.

Send the same `header` on every update and end. Put anything that changes in the status fields.

Two more things are worth planning around:

*   **On iOS, your app can’t update a notification that your server started**, or one that was running before the app restarted. Both lose the in-process handle.
*   **Ending is idempotent.** Ending an unknown or already-ended ID succeeds without doing anything, so a retry is safe.

## Notification types you can start from your app

Customer.io can track three live notification types: the two built-in templates and one custom type.

This isn’t a limit on what your app can show—activities your app runs through ActivityKit or the notification manager on its own are unaffected. It’s just a limit on what Customer.io can track. Starting a type your app didn’t enable fails.

When your server starts a notification, the native SDK renders whatever type the payload names.

For the fields each built-in template accepts, see the [payload reference](/integrations/sdk/flutter/live-notifications/reference/).

---

## Push notifications > App groups

**Source:** /integrations/sdk/flutter/push-notifications/app-groups

# App Groups for push tracking

You need App Groups for reliable push delivery tracking on iOS. Without this setup, delivery metrics may be lost if iOS terminates the Notification Service Extension before the tracking request completes. With App Groups configured, the SDK automatically recovers any lost metrics on the next app launch.

## Before you begin

Before you configure App Groups, make sure you’ve completed the following:

*   [Set up push notifications](/integrations/sdk/flutter/push-notifications/push-setup/) in your app, including the Notification Service Extension (NSE)

## 1\. Add the App Group capability in Xcode

You need to add the App Groups capability to both your main app target and your Notification Service Extension target in Xcode.

Automatic signingManual signing

#### Automatic signing

If you use automatic signing (the most common setup), this is the only step outside of code. Xcode registers the group and updates provisioning profiles automatically.

1.  In Xcode, select your **main app target** > **Signing & Capabilities** > **\+ Capability** > **App Groups**.
2.  Click **+** and enter your group identifier—for example, `group.com.example.myapp.cio`.
3.  Select your **Notification Service Extension target** > **Signing & Capabilities** > **\+ Capability** > **App Groups**.
4.  Select the **same** App Group you created in step 2.

Both targets must reference the exact same App Group string.

#### Manual signing

If you use manual signing, you need to register the group in the Apple Developer Portal and regenerate your provisioning profiles.

1.  Sign in to the [Apple Developer Portal ↗](https://developer.apple.com/) and go to **Certificates, Identifiers & Profiles**.
2.  Click **Identifiers** > **+** > **App Groups**.
3.  Enter your identifier. It must start with `group.`—for example, `group.com.example.myapp.cio`.
4.  Under **Identifiers**, select your main app’s App ID, enable **App Groups**, click **Configure**, and select your group.
5.  Repeat step 4 for your Notification Service Extension’s App ID.
6.  Regenerate provisioning profiles for both your main app and Notification Service Extension. Enabling App Groups invalidates your existing provisioning profiles.

Note

**You must regenerate your provisioning profiles** in the Apple Developer Portal after enabling App Groups. You don’t need to regenerate certificates.

Already have an App Group?

You can reuse an existing App Group by passing its identifier to `.appGroupId()`. There’s no conflict in having multiple App Groups on a target.

## 2\. Pass the App Group ID in your SDK configuration

After you add the App Group capability, pass the App Group ID to the SDK in both your host app and your Notification Service Extension. Both are required—App Groups work by sharing storage between the two targets, so the SDK needs the identifier in each place to read and write delivery metrics. The App Group ID must be identical in both places and must match the entitlements you set up in Xcode.

Note

The `.appGroupId()` method is configured in native Swift code, not in Dart. The Notification Service Extension runs as a separate iOS process outside the Flutter runtime.

### Host app initialization

In your `AppDelegate.swift` file, add `.appGroupId()` to the `MessagingPushFCM.initialize` call:

```
MessagingPushFCM.initialize(
    withConfig: MessagingPushConfigBuilder()
        .appGroupId("group.com.example.myapp.cio")
        .build()
)
```

### Notification service extension initialization

In your `NotificationService.swift` file, add `.appGroupId()` to the `MessagingPushFCM.initializeForExtension` call:

```
MessagingPushFCM.initializeForExtension(
    withConfig: MessagingPushConfigBuilder(cdpApiKey: "YOUR_CDP_API_KEY")
        .appGroupId("group.com.example.myapp.cio")
        .build()
)
```

App Group ID must match everywhere

The `.appGroupId()` value must be identical in your host app initialization, your NSE initialization, and the App Group entitlements on both targets. A mismatch prevents the SDK from accessing shared storage.

### Fallback behavior

If you omit `.appGroupId(...)`, the SDK attempts to infer the identifier using `group.{bundleId}.cio`. This can work as a fallback, but we recommend explicitly passing the value to avoid configuration issues.

---

## Push notifications > Deep links

**Source:** /integrations/sdk/flutter/push-notifications/deep-links

# Deep links

## How it works

Deep links are the links that directs users to a specific location within a mobile app. When you set up your notification, you can set a “deep link.” When your audience taps the notification, the SDK will route users to the right place.

Deep links help make your message meaningful, with a call to action that makes it easier, and more likely, for your audience to follow. For example, if you send a push notification about a sale, you can send a deep link that takes your audience directly to the sale page in your app.

However, to make deep links work, you’ll have to handle them in your app. We’ve provided instructions below to handle deep links in both Android and iOS versions of your app.

## Android: Set up deep links

Deep links provide a way to link to a screen in your app. You’ll set up deep links by adding [intent filters ↗](https://developer.android.com/training/app-links/deep-linking) to the `AndroidManifest.xml` file. Visit Flutter’s documentation for more info on [deep linking ↗](https://docs.flutter.dev/development/ui/navigation/deep-linking).

```
<intent-filter android:label="deep_linking_filter">
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    <!-- Accepts URIs that begin with "myapp://home" -->
    <data
        android:host="home"
        android:scheme="myapp" />
</intent-filter>
```

After you set up intent filters, you can test your implementation with the Rich Push editor or the payloads included for [Testing push notifications](/integrations/sdk/flutter/push-notifications/push/#rich-push-payloads).

### Push Click Behavior (Android)

When you initialize the SDK, you can set a `pushConfig` that controls how your app behaves when your audience taps push notifications on Android devices. The SDK automatically tracks `Opened` metrics for all options.

```
CustomerIO.initialize(
  config: CustomerIOConfig(
    // other config options
    pushConfig: PushConfig(
      android: PushConfigAndroid(
        pushClickBehavior:
        PushClickBehaviorAndroid.activityPreventRestart,
      ),
    ),
  ),
);
```

The available options are:

*   `activityPreventRestart` **(Default)**: If your app is already in the foreground, the SDK will not re-create your app when your audience clicks a push notification. Instead, the SDK will reuse the existing activity. If your app *is not* in the foreground, we’ll launch a new instance of your deep-linked activity. We recommend that you use this setting if your app has screens that your audience shouldn’t navigate away from—like a shopping cart screen.
    
*   `activityNoFlags`: If your app is in the foreground, the SDK will re-create your app when your audience clicks a notification. The activity is added on top of the app’s existing navigation stack, so if your audience tries to go back, they will go back to where they previously were.
    
*   `resetTaskStack`: No matter what state your app is in (foreground, background, killed), the SDK will re-create your app when your audience clicks a push notification. Whether your app is in the foreground or background, the state of your app will be killed so your audience cannot go back to the previous screen if they press the back button.
    

## iOS: Set up deep links

Deep links let you open a specific page in your app instead of opening the device’s web browser. Want to open a screen in your app or perform an action when a push notification or in-app button is clicked? Deep links work great for this!

Setup deep linking in your app. There are two ways to do this; you can do both if you want.

*   **Universal Links**: universal links let you open your mobile app instead of a web browser when someone interacts with a *URL on your website*. For example: `https://your-social-media-app.com/profile?username=dana`—notice how this URL is the same format as a webpage.
*   **App scheme**: app scheme deep links are quick and easy to setup. Example of an app scheme deep link: `your-social-media-app://profile?username=dana`. Notice how this URL is *not* a URL that could show a webpage if your mobile app is not installed.

Universal Links provide a fallback for links if your audience doesn’t have your app installed, but they take longer to set up than *App Scheme* deep links. App Scheme links are easier to set up but won’t work if your audience doesn’t have your app installed.

### Setup App Scheme deep links

Before you can follow this process, you need to set up your app link scheme for iOS. [Learn more about URL schemes for iOS apps ↗](https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app).

1.  Open your project in Xcode and select your root project in the Project Navigator.
2.  Go to the **Info** tab.
3.  Scroll down to the options in the *Info* tab and expand **URL Types**.
4.  Click to add a new, untitled schema.
5.  Under **Identifier** and **URL Schemes**, add the name of your schema.
    
     ![set up your deep link scheme in xcode](https://customer.io/images/xcode-deep-link-schema-flutter.png)
    

### Set up Universal Links

Follow [Flutter’s documentation ↗](https://docs.flutter.dev/development/ui/navigation/deep-linking) to implement Universal Links in your app.

### UIScene routing (iOS 27)

If your app keeps the AppDelegate-only lifecycle, deep links behave exactly as described above and no change is required. The notes below apply when your app adopts iOS’s `UIScene` lifecycle. For the migration steps, see [Set up push notifications](/integrations/sdk/flutter/push-notifications/push-setup/#uiscene-migration) and the [iOS 27 UIScene migration guide](/integrations/sdk/ios/whats-new/ios-27-uiscene-migration/).

Under `UIScene`, with the standard `UIApplicationSceneManifest` present and Flutter deep linking enabled (`FlutterDeepLinkingEnabled` not set to `false`):

*   **Customer.io push and in-app destinations route through Flutter automatically.** The plugin installs its deep-link callback during plugin registration, so a cold push tap that arrives before `CustomerIO.initialize` runs is still delivered to your Dart router.
*   **Ordinary links stay owned by Flutter.** Customer.io does not claim links it did not issue.
*   **Live Activity links** are unwrapped and attributed by Customer.io, which then offers the resulting destination to Flutter. In a scene host, `http` and `https` destinations from the SDK also reach the Dart router, so **your host router owns unknown-route and browser-opening policy**.
*   A **custom `SceneDelegate` must forward Flutter’s scene lifecycle callbacks**. Subclass `FlutterSceneDelegate`, or conform to Flutter’s `FlutterSceneLifeCycleProvider` and forward every callback. Otherwise Flutter (and Customer.io routing through it) never sees the scene events.

Warning

Don’t set the native `SDKConfigBuilder.deepLinkCallback` in a Flutter host. The Flutter plugin owns the SDK’s single callback slot and uses it to deliver Customer.io destinations to Dart.

Advanced: FlutterDeepLinkingEnabled = false

If you set `FlutterDeepLinkingEnabled` to `false`, the plugin registers **neither** its scene adapter **nor** its SDK deep-link callback. Your host then owns **all** Customer.io destinations, including push taps, and must process Live Activity URLs before your own router handles them.

Only a single simultaneous window scene is supported. Multi-window scene routing is out of scope for this release.

---

## Push notifications > Multiple push providers

**Source:** /integrations/sdk/flutter/push-notifications/multiple-push-providers

# Handling multiple push providers

## How to handle multiple push providers

If you use another push service alongside our SDK (like `FlutterFire`), then that other service takes over push handling by default and prevents your app from receiving rich push notifications from Customer.io.

There are two ways to solve this problem, but we typically recommend [the first option](#cio-process-notification-payloads), because it’s more flexible and lets you process notifications through another service. The second option causes our SDK to take over push handling entirely.

## Option 1 (Recommended): Let Customer.io process notification payloads

You can pass the payloads of other message services to Customer.io whenever a device receives a notification, so our SDK can process it for you. The SDK exposes the `onMessageReceived` and `onBackgroundMessageReceived` methods for this purpose. A `true` value (the default) means that the Customer.io SDK will generate the notification and track associated metrics. A `false` value means that the SDK will only process the notification to track metrics but *will not* generate a notification on the device.

App in foregroundApp in background

#### App in foreground

```
CustomerIO.messagingPush().onMessageReceived(payload).then((handled) {
  // handled is true if notification was handled by Customer.io SDK; false otherwise
  return handled;
});
```

#### App in background

```
CustomerIO.messagingPush().onBackgroundMessageReceived(payload).then((handled) {
  // handled is true if notification was handled by Customer.io SDK; false otherwise
  return handled;
});
```

Imagine that you use FlutterFire (Firebase for Flutter) alongside our SDK. You might use the `onMessageReceived` and `onBackgroundMessageReceived` methods to [handle notifications ↗](https://firebase.google.com/docs/cloud-messaging/flutter/receive#message_handling) like this:

FlutterFire foreground exampleFlutterFire background example

#### FlutterFire foreground example

```
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
  CustomerIO.messagingPush().onMessageReceived(message.toMap()).then((handled) {
    // handled is true if notification was handled by Customer.io SDK; false otherwise
    return handled;
  });
});
```

#### FlutterFire background example

```
// Annotation is required only for Flutter version 3.3.0 or higher (to prevent removal during tree shaking in release mode)
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  await Firebase.initializeApp();
  CustomerIO.messagingPush().onBackgroundMessageReceived(message.toMap()).then((handled) {
    // handled is true if notification was handled by Customer.io SDK; false otherwise
    return handled;
  });
}

void main() async {
  // Initialize required SDKs
  FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
  // Run the app
}
```

## Option 2: Register Customer.io Messaging Service

If you add another push service along with our SDK, like FlutterFire (Firebase for Flutter), it will take over push notification handling and prevent your app from receiving rich push notifications from Customer.io. To fix this issue, you need to add the following code under the `<application>` tag in the `Manifest.xml` file in your app’s `Android` folder.

```
<service
    android:name="io.customer.messagingpush.CustomerIOFirebaseMessagingService"
    android:exported="false">
    <intent-filter>
        <action android:name="com.google.firebase.MESSAGING_EVENT" />
    </intent-filter>
</service>
```

This method causes the Customer.io SDK to handle all your push notifications

If you use the code above:

*   Your app will receive all simple and rich push notifications from Customer.io.
*   When your app is in the background, it can receive push notifications with a `notification` payload from other services.
*   Your app **cannot receive** `data`\-only push notifications from another service.

---

## Push notifications > Push metrics

**Source:** /integrations/sdk/flutter/push-notifications/push-metrics

# Capture push metrics

If you've already set up rich push capabilities with the Flutter SDK, you're ready to go. For more advanced use cases, see this document.

## Automatic push handling

Customer.io supports device-side metrics that help you determine the efficacy of your push notifications: `delivered` when a push notification is received by the app and `opened` when a push notification is clicked.

The SDK automatically tracks `opened` and `delivered` events for push notifications originating from Customer.io after you configure your app to receive [push notifications](/integrations/sdk/flutter/push-notifications/push/#register-push). You don’t have to add any code to track `opened` push metrics or launch deep links.

Improve delivery metric reliability

Configure [App Groups](/integrations/sdk/flutter/push-notifications/app-groups/) to make sure delivery metrics aren’t lost when iOS terminates the Notification Service Extension before the tracking request completes. With App Groups, the SDK automatically recovers any undelivered metrics on the next app launch.

Do you use multiple push services in your app?

The Customer.io SDK only handles push notifications that originate from Customer.io. Push notifications that were sent from other push services or displayed locally on device are not handled by the Customer.io SDK. You must add custom handling logic to your app to handle those push events.

Read the sections below to see how you can add (optional) custom handling to various push events.

#### Choose whether to show push while your app is in the foreground

If your app is in the foreground and the device receives a Customer.io push notification, your app gets to choose whether or not to display the push.

To configure this behavior, add the following highlighted line of code in your `AppDelegate.swift` file:

```
func application(
   _ application: UIApplication,
   didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
   ... 
   MessagingPushFCM.initialize(
    withConfig: MessagingPushConfigBuilder()
        .showPushAppInForeground(true) // `true` will display the push when app in foreground
        .build()
   )

   return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
```

If the push did not come from Customer.io, you’ll need to [perform custom handling](#handle-push-received-foreground) to determine whether to display the push or not.

#### Custom handling when users click a push

You might need to perform custom handling when a user clicks a push notification—like when you want to process custom fields in your push notification payload.

For now, the Flutter SDK does not provide callbacks when your audience clicks a push notification. But you can use one of the many popular Flutter push notification SDKs to receive a callback.

For example, the code below receives callbacks when users click a push using FlutterFire. Be sure to follow the documentation for the push notification SDK you choose to use to receive callbacks with.

```
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';

FirebaseMessaging.instance.getInitialMessage().then((initialMessage) {
   // Handle push notification that was clicked, when app was in the killed state 
});

FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
   // Handle push notification that was clicked, when app was in the background state 
});
```

Do you use deep links?

If you’re performing custom push click handling on push notifications originating from Customer.io, we recommend that you don’t launch a deep link URL yourself. Instead, let our SDK launch deep links to avoid unexpected behaviors.

#### Custom handling when getting a push while the app is foregrounded

If your app is in the foreground and you get a push notification, your app gets to choose whether or not to display the push. For push notifications originating from Customer.io, [your SDK configuration determines if you show the notification](#show-push-app-foreground). But you can add custom logic to your app when this kind of thing happens.

For now, the Flutter SDK does not provide callbacks when a push notification is received and your app is in the foreground. But you can use one of the many popular Flutter push notification SDKs to receive a callback.

For example, the code below receives a callback using FlutterFire. Be sure to follow the documentation for the push notification SDK you choose to use to receive callbacks with.

```
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';

FirebaseMessaging.onMessage.listen((RemoteMessage message) {
   // Handle push notification received while app in foreground 
});
```

## Manually track push metrics

You can manually parse a push notification payload and send `opened` or `delivered` events to the SDK:

```
// extracted from payload received in the push notification. 
const deliveryID = '123'; 

// extracted from payload received in the push notification
const deviceToken = 'abc'; 

// or MetricEvent.delivered
const event = MetricEvent.opened;
CustomerIO.instance.trackMetric(
      deliveryID: deliveryID, deviceToken: deviceToken, event: event);
```

---

## Push notifications > Push notification channel

**Source:** /integrations/sdk/flutter/push-notifications/push-notification-channel

# Android push notification channels

🎉New in v2.4.0

Starting in Android 8.0, you can set up “notification channels,” which categorize notifications for your Android app. Every notification now belongs to a channel and the channel determines the behavior of notifications—whether they play sounds, appear as heads-up notifications, and so on. Channels also give users control over which channels they want to see notifications from. For example, if you had a news app, you might have different channels for sports, entertainment, and breaking news, giving users the ability to pick the channels they care about.

Today, Customer.io supports **a single channel per app**, and it has three settings, listed in the table below. You can customize your channel when you first set up the Customer.io SDK, but you cannot change the channel ID or importance level after you’ve created a channel. You can only change the channel name. [Learn more from the official Android developer docs ↗](https://developer.android.com/develop/ui/views/notifications/channels).

Channels are created on the audience’s side when they receive their first push from Customer.io. Users can see your channel in their device settings.

Channel setting

Default

Description

Channel ID

`[your package name]`

The ID of the channel.

Channel name

`[your app name] Notifications`

The name of the channel.

Importance

`3`

The importance of the channel. Acceptable values are `0` (min), `1` (low), `2` (medium), `3` (default/high), and `4` (urgent). See [the Android developer documentation ↗](https://developer.android.com/develop/ui/views/notifications/channels#importance) for more about the behavior of each importance level.

## Channel configuration

When you first integrate with the Customer.io SDK, you can set up your Android channel. Remember, after you’ve released a version of your app with channel settings, you can only change the channel name. Changes to other settings have no effect.

You’ll customize your channel in your app’s manifest.

```
<manifest>
    <application>
        <meta-data
            android:name="io.customer.notification_channel_id"
            android:value="channel_id_value" />
        <meta-data
            android:name="io.customer.notification_channel_name"
            android:value="Channel Name" />
        <meta-data
            android:name="io.customer.notification_channel_importance"
            android:value="4" />
    </application>
</manifest>
```

### What channel settings can I change?

When you first set up the Customer.io Flutter SDK, you can customize your channel. But after you release a version of your app with the Customer.io SDK, you cannot change the channel ID or importance level. After that, you can only change the channel name. This is a limitation imposed by Android, not Customer.io.

If you released your app with a version of the Customer.io Flutter SDK prior to 2.4.0, you can delete your old channel and create a new one with completely new settings per [Android's developer documentation](https://developer.android.com/develop/ui/views/notifications/channels#DeleteChannel).

The chart below shows what channel settings you can or can't change:

## Delete a channel

If you’ve released a version of your app with the Customer.io SDK earlier than v2.4.0, you can delete your old channel and create a new one with completely new settings per [Android’s developer documentation ↗](https://developer.android.com/develop/ui/views/notifications/channels#DeleteChannel).

```
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val id: String = context.packageName
notificationManager.deleteNotificationChannel(id)
```

---

## Push notifications > Push setup

**Source:** /integrations/sdk/flutter/push-notifications/push-setup

# Set up push notifications

Our Flutter SDK supports push notifications over FCM—including rich push messages with links and images. This page helps you add push support to your app.

## How it works

If you’ve followed our [getting started instructions](/integrations/sdk/flutter/getting-started/#install), you’re already set up to send push notifications to your Android audience. But you’ll need to add a bit of code to support your iOS users.

Remember that a device/user can’t receive a push notification until you:

1.  (iOS) Register a device token for the device; code samples on this page help you do that.
2.  [Identify a profile](/integrations/sdk/flutter/identify/). This associates a token with the profile; you can’t send push notifications to a device until you identify the recipient.
3.  (Both iOS and Android) Check for notification permissions. If your app user doesn’t grant permission, notifications will not appear in the system tray.
4.  (Optional) Set up your app to report push metrics back to Customer.io.

Did you already set up your push providers?

To send, test, and receive push notifications, you’ll need to set up your push notification service(s) in Customer.io. If you haven’t already, set up [Firebase Cloud Messaging (FCM)](/messaging/channels/push/getting-started/#for-android).

## Set up push for Android

If you followed our [Getting Started instructions](/integrations/sdk/flutter/getting-started/), you’re already set up to send push notifications to Android devices. You just need to [set up iOS push support](#register-push) in your app.

Next, you can set up [deep links](/integrations/sdk/flutter/push-notifications/deep-links/#deep-links-android) if you want your notifications to link into your app.

### Set or change your push icon

You’ll set the icon that appears on push notifications as a part of the `AndroidManifest.xml` file in your app’s `android` folder. If your icon appears in the wrong size, or if you want to change the standard icon that appears with your push notifications, you’ll need to update your app’s manifest.

```
<meta-data
    android:name="com.google.firebase.messaging.default_notification_icon"
    android:resource="@drawable/ic_notification" />
<meta-data
    android:name="com.google.firebase.messaging.default_notification_color"
    android:resource="@color/colorNotificationIcon" />
```

## Set up push for iOS

You’ll need to add some additional code to support push notifications for iOS. You’ll need to add push capabilities in XCode and integrate push capabilities in your app.

### Add push capabilities in Xcode

Before you can work with push notifications, you need to add Push Notification capabilities to your project in XCode.

1.  In your Flutter project, go to the `ios` subfolder and open `<yourAppName>.xcworkspace`.
    
2.  Select your project. Under *Targets*, select your main app.
    
3.  Click the **Signing & Capabilities** tab and click **Capability**.
    
4.  Add **Push Notifications** to your app.
    
     ![add push notification capabilities to your app](https://customer.io/images/react-native-xcode-push.png)
    
5.  Select **File** > **New** > **Target**.
    
     ![](https://customer.io/images/xcode-servicenotification1.png)
    
6.  Select *Notification Service Extension* and click *Next*.
    
     ![](https://customer.io/images/xcode-servicenotification2.png)
    
7.  You should see a window such as this:
    
     ![](https://customer.io/images/xcode-servicenotification3.png)
    
    You can leave many of the options in this window as their defaults, but you should:
    
    *   Enter a product name, like *NotificationServiceExtension* (which we use in our examples on this page)
    *   Confirm that your main app is selected in the *Embed in Application* drop-down menu
    
    When you’re done, click **Finish**.
    
8.  When presented with the dialog below, click **Cancel**. This will help Xcode continue debugging your app and not just the extension you just added.
    
    ![](https://customer.io/images/xcode-servicenotification4.png)
    

Now you have another target in your project navigator named `NotificationServiceExtension`. We’ll configure this extension when we [Integrate Push Notifications](#swift-push) in the following section.

### Integrate push capabilities in your app

#### Install dependencies

You can install iOS dependencies using CocoaPods or Swift Package Manager (SPM).

Switching to SPM?

If you’re migrating from CocoaPods to SPM, see [Upgrade from 3.x to 4.0.0](/integrations/sdk/flutter/whats-new/4.0.0-upgrade/) for step-by-step instructions.

CocoaPodsSwift Package Manager (SPM)

#### CocoaPods

If you previously enabled SPM, make sure it’s disabled by running `flutter config --no-enable-swift-package-manager`.

1.  Open the file `ios/Podfile` and make the following modifications:
    
    ```
    target 'Runner' do # Look for the main app target.
      # Required by FCM push notification service
      use_frameworks!
    
      # Make all file modifications after these lines:
      config = use_native_modules!
    
      # Add the following line to add the Customer.io native dependency:
      pod 'customer_io/fcm', :path => '.symlinks/plugins/customer_io/ios
    end
    
    # Next, copy and paste the code below to the bottom of your Podfile:
    target 'NotificationServiceExtension' do
      pod 'customer_io_richpush/fcm', :path => '.symlinks/plugins/customer_io/ios'
    end
    ```
    
2.  Run `pod install --repo-update --project-directory=ios` from the root directory of your Flutter project. When dependencies finish installing, you should see a message like this:
    
    ```
    Pod installation complete! There are X dependencies from the Podfile and Y total pods installed.
    ```
    

#### Swift Package Manager (SPM)

Before you start, make sure you meet the following requirements:

*   Flutter 3.24+
*   Xcode 16.0+
*   `firebase_core: ^3.5.0` and `firebase_messaging: ^15.2.0` or higher (older versions don’t support SPM). We recommend at least `firebase_core: ^4.6.0` and `firebase_messaging: ^16.1.3`.

1.  **Enable SPM (if not already enabled).** Run `flutter config --enable-swift-package-manager`, and then run `flutter pub get`. The Customer.io SDK is automatically resolved - no manual Xcode setup needed for the main app.
    
2.  **Update your Podfile.** Remove any `customer_io` pod lines and the `NotificationServiceExtension` target block from your `ios/Podfile`:
    
    ```
    pod 'customer_io/fcm', :path => ...
    pod 'customer_io/location', :path => ...
    pod 'customer_io_richpush/fcm', :path => ...
    ```
    
    With SPM enabled, Flutter’s pod helper automatically skips SPM-handled plugins.
    
3.  **Set up Rich Push (NSE).**
    
    1.  Open `ios/Runner.xcworkspace` in Xcode.
    2.  Select the **NotificationServiceExtension** target.
    3.  Go to **General** > **Frameworks and Libraries** > click **+** and add **FlutterGeneratedPluginSwiftPackage** if it isn’t already added.
    
     ![Add FlutterGeneratedPluginSwiftPackage to the NotificationServiceExtension target](https://customer.io/images/xcode-nse-spm-framework.png)
    

The plugin automatically includes the following modules via SPM:

Module

Purpose

DataPipelines

Core SDK

MessagingInApp

In-app messaging

MessagingPushFCM

Push notifications and Firebase integration

The Location module is not included by default. To include it, set `customerio_location_enabled=true` in your `android/gradle.properties` file. This flag controls both Android and iOS. See [Location tracking](/integrations/sdk/flutter/tracking/location/) for details.

#### Configure your app

1.  Update your `AppDelegate.swift` file to handle push notifications:
    
    ```
    import UIKit
    import Flutter
    import CioMessagingPushFCM
    import CioFirebaseWrapper
    import FirebaseMessaging
    import FirebaseCore
    
    @main
    class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {}
    
    class AppDelegate: FlutterAppDelegate {
        override func application(
            _ application: UIApplication,
            didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
        ) -> Bool {
            GeneratedPluginRegistrant.register(with: self)
    
            // Depending on how you install Firebase,
            // you may need to add functions to this file, like:
            // FirebaseApp.configure()
            //
            // Read the official Firebase docs to install Firebase correctly!
            MessagingPushFCM.initialize(
                withConfig: MessagingPushConfigBuilder()
                    // .appGroupId("group.com.example.myapp.cio") // Optional: for reliable delivery tracking
                    .build()
            )
    
            // Add line below only if you want to have custom control over notifications being presented and processed - Customer.io will handle those automatically
            UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate
    
            return super.application(application, didFinishLaunchingWithOptions: launchOptions)
        }
    
        override func application(_ application: UIApplication,
                                  didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
            super.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken)
    
            Messaging.messaging().apnsToken = deviceToken
        }
    }
    ```
    
2.  Open your `NotificationService.swift` file in Xcode and modify it with the highlighted changes below:
    
    ```
     import CioMessagingPushFCM
     import UserNotifications  // Required when using SPM
    
     class NotificationService: UNNotificationServiceExtension {
    
       override func didReceive(_ request: UNNotificationRequest,
                               withContentHandler contentHandler:
                               @escaping (UNNotificationContent) -> Void) {
    
         MessagingPushFCM.initializeForExtension(
             withConfig: MessagingPushConfigBuilder(cdpApiKey: "YourCdpApiKey")
                 // Optional: set your Customer.io account region (.US or .EU). Default: US
                 .region(.US)
                 // .appGroupId("group.com.example.myapp.cio") // Optional: for reliable delivery tracking
                 .build()
         )
    
         MessagingPush.shared.didReceive(request, withContentHandler: contentHandler)
       }
    
       override func serviceExtensionTimeWillExpire() {
         MessagingPush.shared.serviceExtensionTimeWillExpire()
       }
    
     }
    ```
    
    Note
    
    `import UserNotifications` is required when using SPM. CocoaPods transitively imports system frameworks, so the import isn’t required with CocoaPods, but it doesn’t hurt to include it.
    

Now you can send yourself push notifications with images and deep links to test your implementation. Use a physical device for production APNs or device-specific behavior. For sandbox APNs, an iOS 16 or newer Simulator on macOS 13 or newer can receive remote notifications when its physical Mac has Apple silicon or an Apple T2 Security Chip. A simulator inside a virtualized macOS runner may not receive an APNs token.

### Adopt UIScene (iOS 27)

The `AppDelegate.swift` setup above is the **no-change path**. If your app keeps the AppDelegate-only lifecycle, you don’t need to do anything else here.

Follow the steps below only when your app adopts iOS’s `UIScene` lifecycle or you build with Xcode 27.

Which versions do I need?

Use `customer_io` 4.4.0 or newer. Flutter’s `UIScene` APIs are available from **Flutter 3.38** and become the default in **Flutter 3.41**. Customer.io supports scene hosts on **Flutter 3.44.8 or newer**. This version also contains a separate Flutter build-tool fix required for **Xcode 27**. AppDelegate-only apps keep the published `customer_io` package minimum.

Flutter owns the framework migration. Follow Flutter’s official [UIScene adoption guide ↗](https://docs.flutter.dev/release/breaking-changes/uiscenedelegate). Flutter only auto-migrates an unmodified template AppDelegate. Customer.io setup customizes that file, so follow Flutter’s manual migration steps. The rest of this section describes the Customer.io-specific result. For the full cross-platform walkthrough, see the [iOS 27 UIScene migration guide](/integrations/sdk/ios/whats-new/ios-27-uiscene-migration/).

After migration, your iOS host keeps `CioAppDelegateWrapper` as the application delegate, and the wrapped `AppDelegate` conforms to `FlutterImplicitEngineDelegate`. Move `GeneratedPluginRegistrant` registration out of `didFinishLaunchingWithOptions` and into `didInitializeImplicitFlutterEngine`:

```
import UIKit
import Flutter
import CioMessagingPushFCM
import CioFirebaseWrapper
import FirebaseMessaging
import FirebaseCore

@main
class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {}

@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
    // Register your Flutter plugins here, instead of in didFinishLaunchingWithOptions.
    func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
        GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
    }

    override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        // Customer.io init, APNs, and notification handling stay at the application level.
        // FirebaseApp.configure()
        MessagingPushFCM.initialize(
            withConfig: MessagingPushConfigBuilder()
                // .appGroupId("group.com.example.myapp.cio") // Optional: reliable delivery tracking
                .build()
        )

        UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate

        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }

    override func application(_ application: UIApplication,
                             didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        super.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken)
        Messaging.messaging().apnsToken = deviceToken
    }
}
```

Use `FlutterSceneDelegate` directly as your scene delegate. Add this standard single-scene `UIApplicationSceneManifest` manually:

```
<key>UIApplicationSceneManifest</key>
<dict>
    <key>UIApplicationSupportsMultipleScenes</key>
    <false/>
    <key>UISceneConfigurations</key>
    <dict>
        <key>UIWindowSceneSessionRoleApplication</key>
        <array>
            <dict>
                <key>UISceneClassName</key>
                <string>UIWindowScene</string>
                <key>UISceneDelegateClassName</key>
                <string>FlutterSceneDelegate</string>
                <key>UISceneConfigurationName</key>
                <string>flutter</string>
                <key>UISceneStoryboardFile</key>
                <string>Main</string>
            </dict>
        </array>
    </dict>
</dict>
```

If you need a custom scene delegate, subclass `FlutterSceneDelegate`:

```
import Flutter
import UIKit

class SceneDelegate: FlutterSceneDelegate {
}
```

Then change `UISceneDelegateClassName` in the manifest to `$(PRODUCT_MODULE_NAME).SceneDelegate`. If you write a custom scene delegate that does **not** subclass `FlutterSceneDelegate`, it must conform to Flutter’s `FlutterSceneLifeCycleProvider` and forward every scene lifecycle callback to Flutter. Point `UISceneDelegateClassName` at your class in that case too.

With the standard scene manifest in place and Flutter deep linking enabled, Customer.io push and in-app destinations route through Flutter automatically. See [Deep links](/integrations/sdk/flutter/push-notifications/deep-links/#uiscene-routing) for the routing details.

### Sound in push notifications (iOS Only)

When you send a push notification to iOS devices that uses our SDK, you can opt to send the *Default* system sound or no sound at all. If your audience’s phone is set to vibrate, or they’ve disabled sound permissions for your app, the *Default* setting will cause the device to vibrate rather than playing a sound.

In most cases, you should use the *Default* sound setting to make sure your audience hears (or feels) your message. But, before you send sound, you should understand:

1.  Your app needs permission from your users to play sounds. This is done by your app, not our SDKs. [Here’s an example from our iOS sample app ↗](https://github.com/customerio/customerio-ios/blob/main/Apps/APN-UIKit/APN%20UIKit/Util/NotificationUtil.swift#L12-L13) showing how to request sound permissions.
2.  iOS users can go into *System Settings* and disable sound permissions for your app. Enabling the *Default* setting doesn’t guarantee that your audience hears a sound when your message is delivered!

We don't support custom sounds yet

If you want to send a custom sound, you’ll need to handle it on your own, outside the SDK and use a custom payload when you set up your push notifications.

## Display push notifications in the foreground (iOS)

By default, iOS doesn’t display push notifications when your app is in the foreground. The `showPushAppInForeground` configuration flag (set to `true` by default) tells iOS to display notifications even when your app is active.

```
MessagingPushFCM.initialize(
    withConfig: MessagingPushConfigBuilder()
        .showPushAppInForeground(true)
        .build()
)
```

Note

For more information about handling push notifications in the foreground, including custom handling for non-Customer.io push notifications, see [push metrics and custom handling](/integrations/sdk/flutter/push-notifications/push-metrics/#show-push-app-foreground).

### Implement the notification center delegate for more control

For more granular control over foreground notification behavior, you can implement the `userNotificationCenter(_:willPresent:withCompletionHandler:)` method in your `AppDelegate.swift` file. This native implementation takes precedence over the `showPushAppInForeground` configuration flag.

This method lets you inspect the notification content and decide whether to display it, and choose which presentation options to use (banner, list, badge, sound, or any combination).

Add this method to the `AppDelegate` class you set up in the [integration step](#swift-push) above:

```
override func userNotificationCenter(
    _ center: UNUserNotificationCenter,
    willPresent notification: UNNotification,
    withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
) {
    // Show notifications in foreground with banner, list, badge, and sound
    completionHandler([.banner, .list, .badge, .sound])

    // Or return empty array if you do NOT want notifications shown in foreground
    // completionHandler([])
}
```

## Test your implementation

After you set up rich push, you should test your implementation. Below, we show the payload structure we use for iOS and Android. In general, you can use our regular rich push editor; it’s set up to send messages using the JSON structure we outline below.

If you want to fashion your own payload, you can use our [custom payload](/messaging/channels/push/custom-payloads/#getting-started-with-custom-payloads).

 ![the rich push editor](https://customer.io/images/push-preview.png)

  

iOS FCM payloadAndroid payload

#### iOS FCM payload

```
{
  "message": {
    "apns": {
      "payload": {
        "aps": {
          // basic iOS message and options go here
          "mutable-content": 1,
          "alert": {
            "title": "string", //(optional) The title of the notification.
            "body": "string" //(optional) The message you want to send.
           }
        },
        "CIO": {
          "push": {
            "link": "string", //generally a deep link, i.e. my-app://... or https://yourwebsite.com/...
            "image": "string" //HTTPS URL of your image, including file extension
          }
        }
      },
      "headers": {
        // (optional) headers to send to the Apple Push Notification Service.
        "apns-priority": 10
      }
    }
  }
}
```

*   messageobjectrequired
    
    The base object for all FCM payloads.
    
    *   apnsobjectrequired
        
        Defines a payload for iOS devices sent through Firebase Cloud Messaging (FCM).
        
        *   headersobject
            
            Headers defined by [Apple's payload reference](https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/sending_notification_requests_to_apns) that you want to pass through FCM.
            
        *   payloadobjectrequired
            
            Contains a push payload.
            
            *   CIOobject
                
                Contains properties interpreted by the Customer.io iOS SDK.
                
            *   apsobject
                
                A push payload intended for an iOS device.
                
            *   Custom key-value pairs \*any type
                
                Additional properties that you've set up your app to interpret outside of the Customer.io SDK.
                

#### Android payload

```
{
  "message": {
    "data": {
      "title": "string", //(optional) The title of the notification.
      "body": "string", //The message you want to send.
      "image": "string", //https URL to an image you want to include in the notification
      "link": "string" //Deep link in the format remote-habits://deep?message=hello&message2=world
    }
  }
}
```

*   messageone ofrequired
    
    The parent object for all push payloads.
    
    One of:Data object onlyNotification and data object
    
    *   dataobjectrequired
        
        Contains all properties interpreted by the SDK.
        
        *   titlestring
            
            The title of your push notification.
            
        *   bodystring
            
            The body of your push notification.
            
        *   imagestring
            
            The URL of an HTTPS image that you want to use for your message.
            
        *   linkstring
            
            A deep link (to a page in your app), or a link to a web page.
            
    *   androidobject
        
        Contains properties that are **not** interpreted by the SDK but are defined by FCM. You need to write your own code to handle these Android push features.
        
        *   notificationobject
            
            Properties supported specifically by Android on FCM.
            
            *   iconstring
                
                Sets the notification icon to `myicon` for drawable resource `myicon`. If you don't send this key, FCM displays the launcher icon from your app manifest.
                
            *   soundstring
                
                The sound that plays when the device receives the notification. Supports `"default"` or the filename of a sound resource bundled in your app. Sound files must reside in `/res/raw/`.
                
            *   tagstring
                
                An identifier you use to replace an existing notification. If a notification with the same tag is already showing, the new one replaces it. Leave it empty to create a new notification each time.
                
            *   colorstring
                
                The notification's icon color in `#rrggbb` format.
                
            *   click\_actionstring
                
                The action that occurs when a user taps on the notification. Launches an activity with a matching intent filter when a person taps the notification.
                
            *   body\_loc\_keystring
                
                String resource key used to localize the body text. See Android [String resources](https://developer.android.com/guide/topics/resources/string-resource/).
                
            *   body\_loc\_argstring
                
                Variable string values used in place of the format specifiers in `body_loc_key` to localize the body text to the user's current localization. See Formatting and Styling for more information.
                
            *   title\_loc\_keystring
                
                String resource key used to localize the title text. See Android [String resources](https://developer.android.com/guide/topics/resources/string-resource/).
                
            *   title\_loc\_argstring
                
                Variable string values used in place of the format specifiers in `title_loc_key` to localize the title text to the user's current localization. See Formatting and Styling for more information.
                
    

## Next steps

After you set up push notifications, configure [App Groups](/integrations/sdk/flutter/push-notifications/app-groups/) for reliable delivery tracking on iOS. Without App Groups, iOS can end the Notification Service Extension before delivery metrics are sent, which means some data may never reach Customer.io. App Groups provide shared storage so the SDK can recover any lost metrics on the next app launch.

---

## Tracking > Anonymous activity

**Source:** /integrations/sdk/flutter/tracking/anonymous-activity

# Anonymous activity

Before you identify a profile, calls you make to the SDK are associated with an `anonymousId`. When you identify that profile, we reconcile their anonymous activity with the identified profile.

When you identify a person, and we merge anonymous activity with the identified person, the previously-anonymous activity *can* trigger automations and cause your audience to receive messages.

For example, imagine that you have an ecommerce app, and you want to message people who view a specific product. An anonymous user looks at the product in question, goes to a different page, and then logs into your app. When they log in, we merge their anonymous activity with their identified profile, and their previously-anonymous `screen` view triggers the automation you set up for people who visited the product page.

You can return a profile’s anonymous ID at ay time by calling `CustomerIO.shared.anonymousId`.

---

## Tracking > Geofencing

**Source:** /integrations/sdk/flutter/tracking/geofencing

# Geofencing

## How it works

**Geofences** let you react when a person enters or exits a real-world location—a store, a venue, a neighborhood. You define geofences in Customer.io. The SDK downloads them, monitors them on the device, and automatically sends a `Geofence Transition` event to Customer.io when your users—represented by profiles in Customer.io—cross into (or out of) a geofence. You can use that event to trigger journeys and build segments, the same way you use any other event.

In Customer.io, you can group geofences into **geofence sets**. A geofence set is a group of geofences that you act on together. For example, you might group all of your stores into a geofence set, and trigger an automation when a device enters any of the geofences in the set. Each `Geofence Transition` event includes both the geofence crossed and the geofence set it belongs to.

The SDK relies on the operating system’s native geofencing so transitions can arrive while your app is in the background (subject to permissions).

Flutter geofencing builds on the native Android and iOS SDKs, and depends on the [Location module](/integrations/sdk/flutter/tracking/location/), which supplies the device’s current location. This tells the SDK which geofences to monitor.

Event payloads exclude coordinates

For privacy, `Geofence Transition` events don’t include the profile’s precise latitude and longitude—only whether they entered or exited a geofence.

## Prerequisites

Geofencing requires:

*   The **Location module**, which is enabled automatically when you enable geofencing. See [location tracking](/integrations/sdk/flutter/tracking/location/).
*   Location permissions from the person using your app.
*   Location services enabled on the device.
*   Google Play Services on Android devices.

## Enable geofencing in your native projects

1.  Add the following property to your project’s `android/gradle.properties` file:
    
    ```
    customerio_geofence_enabled=true
    ```
    
    This also enables the Location module, which geofencing depends on—you don’t need to set `customerio_location_enabled` separately.
    

CocoaPodsSPM

#### CocoaPods

2.  Add the `geofence` subspec to your Podfile. Open `ios/Podfile` in your Flutter project and add the following line alongside your existing `customer_io` pod:
    
    ```
    pod 'customer_io/geofence', :path => '.symlinks/plugins/customer_io/ios'
    ```
    
    The `geofence` subspec includes the Location module automatically, so you don’t need to add the `location` subspec separately.
    

#### SPM

No additional iOS setup is needed. The `customerio_geofence_enabled` flag you set in `gradle.properties` also controls whether the Geofence module (and the Location module it depends on) is included via SPM on iOS.

If the flag isn’t detected (e.g. in Flutter add-to-app modules or some CI environments), you can set the `CIO_GEOFENCE` environment variable instead:

```
CIO_GEOFENCE=true flutter build ios
```

You can also set this in Android Studio under **Run** > **Edit Configurations** > **Environment variables**.

## Location permissions

Your app requests location permission itself—the SDK never prompts. What the person grants determines whether you detect crossings while your app is closed, so read both platform sections before you design the permission flow.

### iOS

Your iOS app must include the same `Info.plist` entries required by the native iOS SDK:

```
<key>NSLocationWhenInUseUsageDescription</key>
<string>We use your location to send you relevant, location-based messages.</string>

<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>We use your location to send you relevant, location-based messages.</string>
```

#### You want users to *Always* grant location permissions

Your audience must set location authorization to **Always** for geofencing to work the way you probably expect it to. Without this permission—including when your audience has set it to **While Using the App**—your app can’t detect transitions while it’s closed or in the background; it’s unlikely that your audience has your app open and in the foreground when they pass through a geofence.

Authorization

What you get

Always

Full geofencing support. Geofence transitions arrive when the app is in the foreground, in the background, and whenever iOS relaunches your app. The SDK logs `Geofence background delivery active`.

While Using the App

Geofence transitions only arrive while your app is in the foreground. The SDK logs `Geofence registered for foreground delivery only`.

Denied, Restricted, or not yet asked

Geofencing is effectively disabled. The SDK skips registration and logs `Geofence registration skipped: location permission not granted`.

#### Requesting permissions on iOS

iOS doesn’t set the location authorization to **Always** from a single prompt, so you’ll want to plan your permission flow around it:

1.  Request *when-in-use* authorization the first time someone reaches a part of your app where location matters. Explain what they get in return—your `NSLocationWhenInUseUsageDescription` string is the only explanation that iOS shows.
2.  Request *always* authorization later, at a moment when the benefit is obvious—after someone saves a favorite store, or opts into arrival reminders. iOS shows the upgrade prompt on its own schedule, and it may ask the person again whether to keep allowing background location permissions after your app uses location features.

Don’t ask for both permissions when you first launch your app. A person who denies the prompt can only change their mind in **Settings**, and iOS won’t ask a second time.

You don’t need to tell the SDK when permission changes. It watches for authorization changes and registers geofences on its own when someone grants access—whether they answer a late prompt or change the setting in **Settings**.

If users turn off *precise location*, iOS can delay or miss transitions, especially for small geofences.

### Android

Add the following permissions to `android/app/src/main/AndroidManifest.xml`:

```
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
```

#### You want background location permissions

Geofencing requires your user to grant location permissions. In general, you want your users to set **Allow all the time** for the background location permission, so that your app can detect geofence transitions when your app is closed or in the background.

While `ACCESS_FINE_LOCATION` alone is enough to register geofences, and that may give the appearance that geofencing is working, Android only delivers transitions while your app is in the foreground. Since people rarely have your app open at the moment they arrive somewhere, foreground-only access will miss most geofence transitions.

Permission

Behavior if missing

`ACCESS_FINE_LOCATION`

Geofencing is effectively disabled. The SDK can’t register geofences, and logs `Cannot register geofences: ACCESS_FINE_LOCATION not granted`. Coarse location isn’t a substitute.

`ACCESS_BACKGROUND_LOCATION`

Transitions arrive only while your app is in the foreground. The SDK logs `ACCESS_BACKGROUND_LOCATION not granted — transitions will only fire while the app is in the foreground`.

`RECEIVE_BOOT_COMPLETED`

Geofences aren’t restored after a reboot until the person opens your app again.

Background location is a separate grant on Android 10 (API level 29) and later. On earlier versions, `ACCESS_FINE_LOCATION` covers background delivery on its own.

#### Requesting permissions

On Android 11 (API level 30) and later, you can’t request access to background location in the same prompt as foreground location. That means you need to ask for background location in two steps.

1.  Request `ACCESS_FINE_LOCATION` when someone reaches a part of your app where location matters, and explain what they get in return.
2.  Request `ACCESS_BACKGROUND_LOCATION` later, at a moment when the benefit is obvious. On Android 11 and later, this takes the person to system settings, where they choose **Allow all the time** themselves—so tell them what to pick before you send them there.

Call `refreshFromCurrentLocation()` when the user grants the background location permission, so the SDK picks up nearby geofences without waiting for its next scheduled refresh.

If the user downgrades to foreground-only access later, the SDK keeps its registrations but the operating system won’t wake your app when transitioning in or out of a geofence.

Google Play also requires a [declaration and review ↗](https://support.google.com/googleplay/android-developer/answer/9799150) for apps that request background location. You should plan to spend time on this before you release your app.

## Initialize the SDK with geofencing

Add a `geofenceConfig` to your `CustomerIOConfig`. Providing it opts your app into geofence monitoring and enables the [Location module](/integrations/sdk/flutter/tracking/location/) automatically—you don’t need a separate `locationConfig`. By default, the geofence module acquires the location it needs on its own. See [location modes](#location-modes-and-monitoring) below.

```
CustomerIO.initialize(
  config: CustomerIOConfig(
    cdpApiKey: 'your-cdp-api-key',
    geofenceConfig: GeofenceConfig(),
  ),
);
```

### iOS background bootstrap

iOS may relaunch your app in the background for a geofence transition before the Flutter runtime has started. To make sure those events are processed, call `GeofenceModule.bootstrapForBackgroundDelivery()` from your `AppDelegate` in `application(_:didFinishLaunchingWithOptions:)`:

```
#if canImport(CioLocationGeofence)
import CioLocationGeofence
#endif

override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
    // ...

    #if canImport(CioLocationGeofence)
    GeofenceModule.bootstrapForBackgroundDelivery(launchOptions: launchOptions)
    #endif

    // ...
}
```

### Optional: background delivery on iOS

By default, when you enable geofencing, the SDK stores your `cdpApiKey` on the device so geofence events triggered from a background cold start are delivered in real time. Most apps don’t need to change this. To set it explicitly, pass an `iosConfig`:

```
CustomerIO.initialize(
  config: CustomerIOConfig(
    cdpApiKey: 'your-cdp-api-key',
    geofenceConfig: GeofenceConfig(),
    iosConfig: CustomerIOConfigIos(
      allowBackgroundDelivery: true,
    ),
  ),
);
```

When `allowBackgroundDelivery` is off, events are queued and delivered the next time your app enters the foreground.

## The Geofence Transition event

The SDK sends the `Geofence Transition` event automatically when a person enters or exits a geofence. You don’t need to track enter or exit transitions yourself.

When a person crosses a geofence, the SDK sends one event for each geofence set the geofence belongs to, each carrying that set’s `geosetId`. A geofence that isn’t in any geofence set produces a single event without a `geosetId`.

The event’s timestamp reflects when the device received the transition event and preserves that timestamp if delivery is delayed. So a transition captured while the device is offline still lands on the timeline at the moment it occurred.

Property

Description

`transition`

The direction the device crossed the geofence boundary—`enter` or `exit`.

`geofenceId`

Customer.io-generated ID of the geofence. This is different from the `external_id` used for geofence imports.

`geofenceName`

The geofence’s name. Included only when the geofence has one.

`geosetId`

Customer.io-generated ID of the geofence set.

`metadata`

The metadata you define on the geofence in Customer.io. Values keep their types—a numeric value arrives as a number, not a string. Metadata values must be JSON scalars (`string`, `number`, or `boolean`); the SDKs drop array and object values. If the geofence has no metadata, the property is an empty object.

`transitionId`

A unique ID for the transition into or out of the geofence. Events sent for different geofence sets share the same ID, and it stays stable if the SDK retries delivery. To de-duplicate transition events, use `transitionId` and `geosetId` together.

Example event:

```
{
  "name": "Geofence Transition",
  "timestamp": "2026-07-06T17:42:10.482Z",
  "properties": {
    "transition": "enter",
    "geofenceId": "48122",
    "geofenceName": "Downtown flagship store",
    "geosetId": "25678",
    "transitionId": "b3f1c2a4-8e7d-4c2b-9f10-2a6f1e0d5c33",
    "metadata": {
      "region": "northeast",
      "priority": 2
    }
  }
}
```

## Location modes and monitoring

Devices can only monitor a limited number of geofences at once, so the SDK monitors the geofences nearest to the user, ranked by distance from the device’s location. The SDK automatically refreshes that set as the device moves. Geofence sets don’t affect monitoring—the SDK ranks individual geofences, and geofence set membership only determines the events it sends.

The geofence module needs a location fix to select that set. The `locationMode` in `GeofenceConfig` controls how it gets one:

Location mode

Behavior

`GeofenceLocationMode.automatic` (default)

The SDK acquires a location fix on its own whenever geofencing needs one and none is already available—for example, when you identify a profile.

`GeofenceLocationMode.manual`

The SDK doesn’t acquire location on its own. Call `CustomerIO.geofence.refreshFromCurrentLocation()` after you identify someone and they grant location permission.

```
CustomerIO.initialize(
  config: CustomerIOConfig(
    cdpApiKey: 'your-cdp-api-key',
    geofenceConfig: GeofenceConfig(
      locationMode: GeofenceLocationMode.automatic,
    ),
  ),
);
```

Location the SDK acquires for geofencing is used for geofencing only—it never produces a `Location Update` event or updates the profile. It also works independently of the Location module’s tracking mode, even when location tracking is off.

`refreshFromCurrentLocation()` works in either mode—call it any time you want to force an immediate refresh. Geofences are tied to the identified profile, so in manual mode call it after you identify someone; a refresh requested before you identify anyone isn’t retried automatically.

Neither mode limits detection to the moment the SDK acquires a location. Once the SDK registers geofences, the operating system monitors them natively—it wakes your app when the device crosses one, in the background and even between launches, without continuously tracking their location.

The device won't see new geofences immediately.

Any changes you make to geofences or geofence sets in Customer.io won’t be available on the device until it fetches updates from Customer.io. This happens within 24 hours for an active app, or sooner if the person travels a long distance, or your app forces a refresh. See [Geofence refresh](#geofence-refresh).

### Geofence refresh

The SDK re-downloads your geofence definitions from Customer.io—picking up any geofences you’ve added, changed, or removed, along with the geofence sets they belong to, when:

*   **Your app launches, or you identify a person**—at most once every 24 hours. Geofencing needs an identified profile, so if no one is identified when your app launches, the fetch waits until you identify someone.
*   **The person travels a long distance** (about 5 km by default) from where the SDK last fetched.
*   **Your app forces a refresh** by calling `refreshFromCurrentLocation()`.

So changes you make in Customer.io aren’t instant. They reach a device the next time one of these happens—typically within a day for an app in regular use, but longer for an app no one opens.

### Duplicate event suppression

The SDK suppresses duplicate enter and exit events for the same geofence within 1 hour. If a device crosses into a geofence, out again, and then crosses back into the geofence within the next hour, the SDK drops the second entrance event.

## Limitations

*   The SDK doesn’t continuously track the profile’s location.
*   Geofencing depends on operating-system callbacks.
*   Background delivery depends on platform permissions.
*   Geofencing needs the device’s location services. If location is turned off—for example, airplane mode with location disabled—the operating system can’t detect crossings until location is available again. Delivery of already-captured transitions resumes when a network connection returns.
*   Android geofencing requires Google Play Services.
*   Mobile operating systems limit how many geofences an app can monitor at once. If your app uses geofencing from more than one source (for example, Customer.io and another SDK), all monitored geofences share the same operating-system limit. The Customer.io SDK manages its own geofences within that limit, but it can’t control geofences registered by other SDKs or by your app.

---

## Tracking > Identify

**Source:** /integrations/sdk/flutter/tracking/identify

# Identify profiles

Use `CustomerIO.identify()` to identify a profile. You need to identify a mobile user before you can send them messages or track events for things they do in your app.

This page is part of a setup flow for the SDK. Before you continue, make sure you've implemented previous features—i.e. you can't identify people before you initialize the SDK!

## Identify a profile

Identifying a person:

1.  Adds or updates the person in your workspace. This is basically the same as an [`identify` call to our server-side API](/integrations/api/track/tag/track-customers/identify/).
2.  Saves the person's information on the device. Future calls to the SDK reference the identified person. For example, after you identify a person, any events that you track are automatically associated with that person.
3.  Associates the current device token with the person.

You can only identify one customer at a time. The SDK "remembers" the most recently-identified customer. If you identify person A, and then call the identify function for person B, the SDK "forgets" person A and assumes that person B is the current app user. You can also [stop identifying a person](#clearIdentify), which you might do when someone logs off or stops using your app for a significant period of time.

An identify request takes two parameters:

*   **userId** (Required): The unique value representing a profile—an ID, email address, or the [cio\_id](/journeys/people/manage/identifying-people/#cio_id). If you treat `phone` as an identifier, you should identify profiles by `id` or `email` and send the phone number as a `phone` attribute.
*   **traits** (Optional): An object containing [attributes](/journeys/people/manage/attributes/) that you want to add to, or update on, a profile

```
CustomerIO.instance.identify(userId: email, traits: {
  "name": user.displayName,
  "email": user.email,
  "age": user.age,
});
```

### Update a profile’s attributes

You store information about a profile in Customer.io as [attributes](/journeys/people/manage/attributes/). When you call the `identify()` function, you can update a profile’s attributes in Customer.io.

If you’ve already identified a profile, and they update their preferences, provide additional information about themselves, or perform other attribute-changing actions, you can update their attributes with `profileAttributes`.

You only need to pass the attributes that you want to create or modify to `setProfileAttributes`. For example, if you identify a new profile with the attribute `{"first_name": "Dana"}`, and then you call `CustomerIO.instance.setProfileAttributes(traits: {"favorite_food": "pizza"});`, the profile will gain the `favorite_food` attribute but `first_name` attribute will still be `Dana`.

```
CustomerIO.instance.setProfileAttributes(traits: {
  "first_name": "Cool",
  "last_name": "User",
  "is_premium": false,
});
```

### Device attributes

By default (if you don’t set `.autoTrackDeviceAttributes(false)` in your config), the SDK automatically collects a series of [attributes](/journeys/people/manage/attributes/) for each device. You can use these attributes in [segments](/journeys/segmentation/data-driven-segments/) and other automation workflow conditions to target the device owner, just like you would use a profile’s other attributes. You cannot, however, use device attributes to personalize messages with [liquid](/journeys/liquid/using-liquid) yet.

Along with these attributes, we automatically set a `last_used` timestamp for each device indicating when the device owner was last identified, and the `last_status` of a push notification you sent to the device. You can also set your own custom device attributes. You’ll see a profile’s devices and each device’s attributes when you go to **Journeys > Profiles > Select a profile**, and click **Devices**.

 ![device attributes on a profile](https://customer.io/images/device-attributes.png)

Your integration shows device attributes in the `context` object

When you inspect calls from the SDK (in your integration’s data in tab), you’ll see device information in the `context` object. We flatten the device attributes that you send into your workspace, so that they’re easier to use in [segments](/journeys/segmentation/segments/). For example, `context.network.cellular` becomes `network_cellular`.

*   idstringrequired
    
    The device token.
    
*   last\_usedinteger(unix timestamp)
    
    The `timestamp` when you last identified this device. If you don't pass a timestamp when you add or update a device, we use the time of the request itself. Our SDKs identify a device when a person launches their app.
    
*   platformstringrequired
    
    The device/messaging platform.
    
    Accepted values: `ios`, `android`
    
*   attributesobject
    
    Attributes that you can reference to segment your audience—like a person's attributes, but specific to a device. These can be either the attributes defined below or custom key-value attributes.
    
    *   device\_osstring
        
        The operating system, including the version, on the device.
        
    *   device\_modelstring
        
        The model of the device a person uses.
        
    *   app\_versionstring
        
        The version of your app that a customer uses. You might target app versions to let people know when they need to update, or expose them to new features when they do.
        
    *   app\_identifierstring
        
        The app that the device belongs to—the **Bundle ID / App identifier** you entered with that app's push credentials. If you have more than one app, we use this attribute to send push notifications to the right app. A device without an app identifier receives push notifications you send to your [default app](/messaging/channels/push/getting-started/#the-default-app).
        
    *   cio\_sdk\_versionstring
        
        The version of the Customer.io SDK in the app.
        
    *   \_last\_statusstring
        
        The delivery status of the last message sent to the device—sent, bounced, or suppressed. An empty string indicates that that the device hasn't received a push yet.
        
        Accepted values: , `bounced`, `sent`, `suppressed`
        
    *   device\_localestring
        
        The device's [IETF language code](/journeys/channels/localization/getting-started/#supported-languages), such as `en-MX` or `es-ES`.
        
    *   push\_enabledstring
        
        If `"true"`, the device is opted-in and can receive push notifications.
        
        Accepted values: `true`, `false`
        
    *   network\_bluetoothboolean
        
        If `true`, the device's bluetooth connection is on.
        
    *   network\_cellularboolean
        
        If `true`, the device's cellular connection is on.
        
    *   network\_wifiboolean
        
        If `true`, the device's WiFi connection is on.
        
    *   screen\_heightinteger
        
        The height of the device's screen in pixels.
        
    *   screen\_widthinteger
        
        The width of the device's screen in pixels.
        
    *   timezonestring
        
        The time zone of the device.
        
    *   Custom Device Attributes \*string
        
        Custom properties that you want to associate with the device.
        

#### Set custom device attributes

You can also set custom device attributes with the `setDeviceAttributes` method. You might do this to save app preferences, time zone, or other custom values specific to the device. Like profile attributes, you can pass nested JSON to device attributes.

However, before you set custom device attributes, consider whether the attribute is specific to the `device` or if it applies to the profile more broadly. Device tokens are ephemeral—they can change based on user behavior, like when a person uninstalls and reinstalls your app. If you want an attribute to persist beyond the life of the device, you should [apply it to the profile](#update-person) rather than the device.

```
const deviceAttributes = {
  "type" : "primary_device",
  "parentObject" : {
    "childProperty" : "someValue",
  },
};
CustomerIO.instance.setDeviceAttributes(attributes: deviceAttributes);
```

#### Disable automatic device attribute collection

By default, the SDK automatically collects the device [attributes](/journeys/people/manage/attributes/) [defined above](#device-attributes). You can disable the `autoTrackDeviceAttributes` setting to prevent the SDK from automatically collecting these attributes.

```
CustomerIO.initialize(
  config: CustomerIOConfig(
      cdpApiKey: '<your API Key>',
      autoTrackDeviceAttributes: false,
      inAppConfig: InAppConfig(siteId: '<your siteId>'),
  ),
);
```

#### Manually add device to profile

In the standard flow, identifying a profile automatically associates the token with the identified profile in your workspace. If you need to manually add or update the device elsewhere in your code, call `CustomerIO.instance.registerDeviceToken(token)`.

## Stop identifying a profile

When a person logs out, or does something else to tell you that they no longer want to be tracked, you should stop identifying them.

Use `clearIdentify()` to stop identifying the previously identified profile (if there was one).

```
CustomerIO.instance.clearIdentify();
```

### Identify a different profile

If you want to identify a new profile—like when someone switches profiles on a streaming app, etc—you can simply call `identify()` for the new profile. The new profile then becomes the currently-identified profile, with whom all new information—messages, events, etc—is associated.

```
CustomerIO.instance.identify(identifier: "new.person@example.com", attributes: {"first_name": "New", "last_name": "Person"}); 
```

---

## Tracking > Lifecycle events

**Source:** /integrations/sdk/flutter/tracking/lifecycle-events

# Mobile lifecycle events

By default, the Customer.io SDK automatically tracks lifecycle events for your users. These are events that represent the lifecycle of your app and your users' experiences with it.

By default, we track the following lifecycle events:

*   **Application Installed**: A user installed your app.
*   **Application Updated**: A user updated your app.
*   **Application Opened**: A user opened your app.
*   **Application Foregrounded**: A user switched back to your app.
*   **Application Backgrounded**: A user backgrounded your app or switched to another app.

You might also want to send your own lifecycle events, like `Application Crashed` or `Application Updated`. You can do this using the `track` call. You’ll find a list of properties for these events—both the ones we track automatically and other events you might send yourself—in our [Mobile App Lifecycle Event specification](/integrations/api/cdp/mobile-app/).

## Lifecycle event examples

A lifecycle event is basically a `track` call that the SDK makes automatically for you.

When you look at your data in Customer.io, you’ll see lifecycle events as `track` calls, where the event `properties` are specific to the name of the event. For example, the `Application Installed` event includes the app `version` and `build` properties.

```
{
  "userId": "app.installer@example.com",
  "type": "track",
  "event": "Application Installed",
  "properties": {
    "version": "3.2.1", "build": "247"
  }
}
```

## Sending custom lifecycle events

You can send your own lifecycle events using the `track` call. However, whenever you send lifecycle events, you should use the *Application EventName* convention that we use for our default lifecycle events.

These [semantic event](/integrations/data-in/semantic-events/) names and properties represent [a standard](/integrations/data-in/semantic-events/mobile-app/) that we use across Customer.io and our downstream destinations. Adhering to this standard ensures that your events automatically map to the correct event types in Customer.io and any other services you send your data to.

If you opt [out of automatic lifecycle events](#disable-lifecycle-events), you can send your own `track` calls for these events. Or, for events we can’t track automatically, you might be able to use a webhook or a callback to collect crash events. For example, you might want to send a `track` call for `Application Crashed` if your app crashes or `Application Updated` when people update your app.

```
CustomerIO.instance.track(
    name: "Application Crashed", 
    properties: [
      "url": "urls://page/in/app"
    ]
)
```

## Disable lifecycle events

We track lifecycle events by default. You can disable this behavior by passing the `trackApplicationLifecycleEvents` option in your configuration.

```
CustomerIO.initialize(
  config: CustomerIOConfig(
      cdpApiKey: '<your API Key>',
      region: Region.us,
      trackApplicationLifecycleEvents: false,
      inAppConfig: InAppConfig(siteId: '<your siteId>'),
  ),
);
```

---

## Tracking > Location

**Source:** /integrations/sdk/flutter/tracking/location

# Location tracking

## How it works

The Location module captures location (with user consent) from your app and attaches it to a profile in Customer.io. You can use this data for geo-aware messaging and audience segmentation with more accuracy than [IP-based geolocation](/messaging/profiles/geolocation/).

When you identify a profile, the SDK includes the latest location in the identify call. The SDK also sends a `Location Update` event to the profile’s activity timeline, which you can use in journeys and segments. To balance location updates with battery and data usage, the SDK limits location updates once a day (at most)—and only sends that update when the person has moved a meaningful distance since the last update.

The SDK does not request location permission on its own—your app must handle the permission flow.

## Install the location module

The Location module requires native dependencies on both iOS and Android. Follow the platform-specific steps below.

1.  Add the following property to your project’s `android/gradle.properties` file:
    
    ```
    customerio_location_enabled=true
    ```
    

CocoaPodsSPM

#### CocoaPods

2.  Add the `location` subspec to your Podfile. Open `ios/Podfile` in your Flutter project and add the following line alongside your existing `customer_io` pod:
    
    ```
    pod 'customer_io/location', :path => '.symlinks/plugins/customer_io/ios'
    ```
    

#### SPM

No additional iOS setup is needed. The `customerio_location_enabled` flag you set in `gradle.properties` also controls whether the Location module is included via SPM on iOS.

If the flag isn’t detected (e.g. in Flutter add-to-app modules or some CI environments), you can set the `CIO_LOCATION` environment variable instead:

```
CIO_LOCATION=true flutter build ios
```

You can also set this in Android Studio under **Run** > **Edit Configurations** > **Environment variables**.

## Initialize the SDK with the location module

Add a `locationConfig` to your `CustomerIOConfig` to enable the module. The `trackingMode` property controls how and when the SDK captures location.

Option

Type

Default

Description

`trackingMode`

`LocationTrackingMode`

`manual`

Controls how and when the SDK captures location. See [tracking modes](#tracking-modes) below.

### Tracking modes

Mode

Description

`LocationTrackingMode.manual`

Your app controls when it captures location. Call `setLastKnownLocation()` or `requestLocationUpdate()` to provide location. Use this when your app already has a location-tracking mechanism or you want full control over when you capture location data.

`LocationTrackingMode.onAppStart`

The SDK automatically captures a one-shot location once per app launch when your app enters the foreground. You can still call `setLastKnownLocation()` or `requestLocationUpdate()` alongside automatic capture. Use this for hands-off location tracking with minimal battery impact.

`LocationTrackingMode.off`

Disables location tracking entirely. All location calls become silent and location is not included in identify calls. Use this if you want to register the module but disable it at runtime.

```
CustomerIO.initialize(
  config: CustomerIOConfig(
    cdpApiKey: 'YOUR_CDP_API_KEY',
    // ...other config options
    locationConfig: LocationConfig(
      trackingMode: LocationTrackingMode.manual,
    ),
  ),
);
```

## Location APIs

The module provides two methods to capture location. You can call either method as often as you like; the SDK always caches the latest coordinates for profile enrichment, but sends a `Location Update` event no more than once a day—and only if the person has moved a meaningful distance since the last update. No matter how frequently you call these methods, the SDK throttles the updates for you so as not to overwhelm your workspace with profile updates.

### `setLastKnownLocation`

Pass coordinates directly from your app’s own location system. This doesn’t require any location permissions from the SDK. Your app manages location access independently of Customer.io.

Parameter

Type

Description

`latitude`

`double`

Latitude in degrees. Must be between -90 and 90.

`longitude`

`double`

Longitude in degrees. Must be between -180 and 180.

```
// Pass coordinates from your app's location provider
CustomerIO.location.setLastKnownLocation(
  latitude: 37.7749,
  longitude: -122.4194,
);
```

### `requestLocationUpdate`

Request a one-shot location from the native platform’s location services. Use this if your app doesn’t have its own location system. Your app must request location permission before calling this method—the SDK won’t prompt the user.

If a user doesn’t grant permission or location services are disabled, the request is ignored—no crash or exception. If a request is already in progress, additional calls are ignored until the current request completes.

1.  Add the required key to your `Info.plist`:
    
    ```
    <key>NSLocationWhenInUseUsageDescription</key>
    <string>We use your location to personalize your experience.</string>
    ```
    
2.  **(Optional)** If you want more precise location, add `ACCESS_FINE_LOCATION` to your `AndroidManifest.xml`. The SDK already includes `ACCESS_COARSE_LOCATION` when the location module is enabled.
    
    ```
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    ```
    
3.  After your app requests and receives permission at runtime, call the SDK:
    
    ```
    // After location permission is granted
    CustomerIO.location.requestLocationUpdate();
    ```
    

Tip

We recommend using a package like [geolocator ↗](https://pub.dev/packages/geolocator) or [location ↗](https://pub.dev/packages/location) to handle location permission requests in your Flutter app.

## Profile switch behavior

When you call `CustomerIO.instance.clearIdentify()`, the SDK clears cached location data so that one profile’s location doesn’t carry over to another profile. The next profile you identify starts with a clean slate.

Location persists across app restarts. When your app relaunches, the SDK restores the cached location so that the next `identify()` call includes it automatically.

---

## Tracking > Screen events

**Source:** /integrations/sdk/flutter/tracking/screen-events

# Screen tracking

Screen views are events that record the pages that your audience visits in your app. They have a `type` property set to `screen`, and a `title` representing the title of the screen or page that a profile visited in your app.

Screen view events let you trigger [automations](/journeys/send/campaigns/overview/) or add profiles to [segments](/journeys/segmentation/data-driven-segments/) based on the parts of your app your they use. Screen events also update your audience’s “Last Visited” attribute, which can help you track how recently profiles used your app.

## Enable automatic screen tracking

We’ve provided some example code below using [Route observer ↗](https://api.flutter.dev/flutter/widgets/RouteObserver-class.html) for automatic screen tracking.

If you want to send more data with screen events, or you don’t want to send events for every individual screen that people view in your app, you [send screen events manually](#manual-screenview).

```
class MyRouteObserver extends RouteObserver<PageRoute<dynamic>> {
  void _sendScreenView(PageRoute<dynamic> route) {
    var screenName = route.settings.name;

    // track screen manually
    CustomerIO.screen(name: screenName ?? "N/A");
  }

  @override
  void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
    super.didPush(route, previousRoute);
    if (route is PageRoute) {
      _sendScreenView(route);
    }
  }

  @override
  void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) {
    super.didReplace(newRoute: newRoute, oldRoute: oldRoute);
    if (newRoute is PageRoute) {
      _sendScreenView(newRoute);
    }
  }

  @override
  void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
    super.didPop(route, previousRoute);
    if (previousRoute is PageRoute && route is PageRoute) {
      _sendScreenView(previousRoute);
    }
  }
}

// Usage
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(),
      navigatorObservers: [MyRouteObserver()],
      home: Screen1(),
      routes: {
        'screen2': (context) => Screen2(),
        'screen3': (context) => Screen3(),
      },
    );
  }
}
```

### Screenview settings for in-app messages

Customer.io uses `screen` events to determine where users are in your app so you can target them with in-app messages on specific screens. By default, the SDK sends `screen` events to Customer.io’s backend servers. But, if you don’t use `screen` events to track user activity, segment your audience, or to trigger automations, these events might constitute unnecessary traffic and event history.

If you don’t use `screen` events for anything other than in-app notifications, you can set the `ScreenViewUse` parameter to `ScreenView.inApp`. This setting stops the SDK from sending `screen` events back to Customer.io but still allows the SDK to use `screen` events for in-app messages, so you can target in-app messages to the right screen(s) without sending event traffic into Customer.io!

```
CustomerIO.initialize(
  config: CustomerIOConfig(
    cdpApiKey: "YOUR_CDP_API_KEY", // Required
    screenViewUse: ScreenView.inApp,
  ),
);
```

## Send your own screen events

Screen events use the `.screen` method. Like other event types, you can add a `properties` object containing additional information about the screen or the user.

```
CustomerIO.instance.screen(title: "screen-name", properties: {"property": "value"});
```

---

## Tracking > Track events

**Source:** /integrations/sdk/flutter/tracking/track-events

# Track events

Events are things people do in your app. They help you track your audience's behaviors, activity, and metrics. You can use them to segment your audience, trigger messaging automations, and see how people use your app.

## Track an event

The `track` method helps you send events representing your audience’s activities to Customer.io. When you send events, you can include event `properties`—information about the person or the event that they performed.

In Customer.io, you can use events to trigger automations and broadcasts. Those automations might send someone a push notification or manipulate information associated with the profile in your workspace.

Events include the following:

*   `name` (Required): The name of the event. Most event-based searches in Customer.io hinge on the name, so make sure that you provide an event name that will make sense to other members of your team.
*   `properties` (Optional): Additional information that you want to reference in messages or use to segment your audience, etc. You can reference event properties in messages and other workflow actions using [liquid](/journeys/liquid/using-liquid) in the format `{{event.<properties>}}`.

```
CustomerIO.instance.track(name: "add-to-cart", properties: {"product": "shoes", "price": "29.99"});
```

### Anonymous activity

If you send a `track` call before you `identify` a profile, we’ll attribute the event to an `anonymousId`. When you identify the profile, we’ll reconcile their anonymous activity with the identified profile.

When we apply anonymous events to an identified profile, the previously anonymous activity becomes eligible to trigger automations in Customer.io.

## Semantic Events

Some actions don’t map cleanly to our simple `identify`, `track`, and other calls. For these, we use “semantic events,” events that have a special meaning in Customer.io and your destinations.

These are especially important in Customer.io for destructive operations like deleting a profile. When you send an event with a semantic `event` name, we’ll perform the appropriate action.

For example, if a person decides to leave your service, you might delete them from your workspace. In Customer.io, you’ll do that with a `Delete Person` event.

For example, you can associate a profile with a group (called an [object](/journeys/objects-data/objects/) in Customer.io) with a simple `group` call. But what if you wanted to remove someone from the group? In Customer.io, you do that with an event called `Relationship Deleted`.

```
CustomerIO.instance.track(name: "User Deleted)
```

---

## Whats new > 2.2 upgrade

**Source:** /integrations/sdk/flutter/whats-new/2.2-upgrade

# 2.x -> 2.2

Version 2.2 of the Customer.io Flutter SDK introduces a new `CioAppDelegateWrapper` pattern for iOS that simplifies push notification setup and eliminates the need for method swizzling.

## Key Changes

The primary change in version 2.2 is the introduction of the wrapper pattern for handling push notifications on iOS. This change:

*   **Eliminates method swizzling**: No more automatic method replacement
*   **Simplifies setup**: Less boilerplate code required
*   **Improves reliability**: More predictable behavior

See the instructions below to update your Flutter app’s iOS configuration.

## Upgrading to SDK 2.2

Update your dependencies:

1.  Update your `pubspec.yaml` file:
    
    ```
    dependencies:
      customer_io: ^2.2.0
    ```
    
2.  Run dependency update:
    
    ```
    flutter pub get && cd ios && pod install --repo-update && cd ..
    ```
    

## Update with FCM (Firebase Cloud Messaging)

Update your `AppDelegate.swift` file to use the new `CioAppDelegateWrapper` pattern. See the *Before* sample to see what needs to change and the *After* sample to see the new pattern.

Before (2.x)After (2.2)

#### Before (2.x)

```
import UIKit
import Flutter
import CioMessagingPushFCM
import FirebaseMessaging
import FirebaseCore

@main
@objc class AppDelegate: FlutterAppDelegate {
    override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        GeneratedPluginRegistrant.register(with: self)
        
        Messaging.messaging().delegate = self
        
        MessagingPushFCM.initialize(
            withConfig: MessagingPushConfigBuilder()
                .build()
        )
        
        UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate

        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }
    
    func application(application: UIApplication,
                    didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        Messaging.messaging().setAPNSToken(deviceToken, type: .unknown);
    }
    
    override func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
        MessagingPush.shared.application(application, didFailToRegisterForRemoteNotificationsWithError: error)
    }
}

extension AppDelegate: MessagingDelegate {
    func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
        MessagingPush.shared.messaging(messaging, didReceiveRegistrationToken: fcmToken)
    }
}
```

#### After (2.2)

```
import UIKit
import Flutter
import CioMessagingPushFCM
import FirebaseMessaging
import FirebaseCore

@main
class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {}

class AppDelegate: FlutterAppDelegate {
    override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        GeneratedPluginRegistrant.register(with: self)
        
        // Initialize push with wrapper - handles all push methods automatically
        MessagingPushFCM.initialize(
            withConfig: MessagingPushConfigBuilder()
                .build()
        )

        // Optional: Add only if you want custom control over notifications
        UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate
        
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }

    override func application(_ application: UIApplication,
                              didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        super.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken)
        
        Messaging.messaging().apnsToken = deviceToken
    }
    
    // No manual push methods needed - CioAppDelegateWrapper handles everything
}
```

## Configuration Options

You can customize the push behavior when you initialize your push package:

```
MessagingPushFCM.initialize(
    withConfig: MessagingPushConfigBuilder()
        .showPushAppInForeground(true)  // Show push when app is in foreground
        .autoTrackPushEvents(true)       // Automatically track push metrics
        .build()
)
```

## Important Notes

1.  **Manual push handling methods are not required**: the `CioAppDelegateWrapper` automatically records information from following methods. But you can still use these methods if you want to add custom push handling:
    
    *   `didRegisterForRemoteNotificationsWithDeviceToken`
    *   `didFailToRegisterForRemoteNotificationsWithError`
    *   All other push-related delegate methods
2.  **The `@main` attribute** - Must be on the wrapper class, not your AppDelegate.
    
3.  **Flutter-specific**: Your Flutter app’s main.dart file doesn’t need any changes.
    
4.  **NotificationService.swift remains unchanged** - Your notification service extension configuration works the same with both approaches.
    

## Troubleshooting

If push notifications stop working after you update your implementation:

1.  Make sure that you’ve added the `@main` attribute to the wrapper class
2.  Verify that you’ve removed `@main` from your original AppDelegate
3.  Check that you’re calling `MessagingPushFCM.initialize()`
4.  Run `flutter clean` followed by `pod install --repo-update --project-directory=ios`
5.  Test on a physical device (push notifications don’t work on simulators)

---

## Whats new > 2.x upgrade

**Source:** /integrations/sdk/flutter/whats-new/2.x-upgrade

# Upgrade to Flutter 2.x

This page provides steps to help you upgrade from our Flutter 1.x SDK so you understand the development effort required to update your app and take advantage of the latest features.

## What changed?

This update provides native support for our new integrations framework. While this represents a significant change “under the hood,” we’ve tried to make it as seamless as possible for you; much of your implementation remains the same.

This move also adds two additional features:

*   **You’ll use SDK methods from an `instance`**:
*   **Support for anonymous tracking**: you can send events and other activity for anonymous users, and we’ll reconcile that activity with a profile when you identify them.
*   **Built-in lifecycle events**: the SDK now automatically captures events like “Application Installed” and “Application Updated” for you.
*   **New device-level data**: the SDK captures the device `name` and other device-level context for you.

*   **A new `group` API call**: this new call helps you relate profiles to [objects](/journeys/objects-data/objects/) in Customer.io and accounts in other, downstream destinations.

## Upgrade process

You’ll update initialization calls for the SDK itself and the push and/or in-app messaging modules.

**As a part of this process, your credentials change**. You’ll need to set up a new data in integration in Customer.io and get a new *CDP API Key*. But you’ll *also* need to keep your previous `siteId` as a `migrationSiteId` when you initialize the SDK. The `migrationSiteId` is a key helps the SDK send remaining traffic when people update your app.

When you’re done, you’ll also need to change a few base properties to fit the new APIs. In general, `identifier` becomes `userId`, `body` becomes `traits`, and `data` becomes `properties`.

### 1\. Get your new *CDP API Key*

The new version of the SDK requires you to set up a new data in integration in Customer.io. As a part of this process, you’ll get your *CDP API Key*.

1.  Go to [*Integrations*](https://fly.customer.io/workspaces/last/journeys/integrations/all/overview) and click **Add Integration**.
2.  Select **Flutter**.
    
     ![set up your Flutter integration](https://customer.io/images/cdp-flutter-source.png)
    
3.  Enter a *Name* for your integration, like “My Flutter App”.
4.  We’ll present you with a code sample containing a `cdpApiKey` that you’ll use to initialize the SDK. Copy this key and keep it handy.
5.  Test your connection and click **Complete Setup**. Or, if you don’t want to test your implementation yet, **Save & Complete Later** and then click **Install Source** to finish the setup process. In this case, *Complete Later* simply means that we haven’t seen any data from your Flutter app yet.
    
     ![Set your name, get your CDP API Key, and click Complete Setup](https://customer.io/images/cdp-flutter-source-setup.png)
    

Remember, you can also [connect your Flutter app to services outside of Customer.io](/integrations/data-out/add-destination/)—like your analytics provider, data warehouse, or CRM.

### 2\. Update the SDK initialization

You’ll need to update the way you initialize the SDK—with a new key and configuration options for in-app and push. We show an example configuration below.

You’ll find a complete list of configuration options on the [*Packages and Configuration Options* page](/integrations/sdk/flutter/getting-started/packages-options/), but you’ll want to pay close attention to the following changes:

*   You’ll initialize the SDK with a `cdpApiKey`. This is the key you’ll get when you create your Flutter integration in Customer.io.
*   `siteId` becomes `migrationSiteId`.
*   Your `inAppConfig` changes and requires your `siteId`.
*   The optional `pushConfig` is now a part of your initialization call. You won’t set push settings as a part of a separate configuration like you did with the 1.x SDK.

```
CustomerIO.initialize(
  config: CustomerIOConfig(
    cdpApiKey: 'cdpApiKey',
    migrationSiteId: 'migrationSiteId',
    region: Region.us,
    autoTrackDeviceAttributes: true,
    inAppConfig: InAppConfig(siteId: 'siteId'),
    // pushConfig is optional if you use default settings
    pushConfig: PushConfig(
      android: PushConfigAndroid(
        pushClickBehaviorAndroid:
        PushClickBehaviorAndroid.activityPreventRestart,
      ),
    ),
  ),
);
```

### 3\. Update your podfile (iOS)

For iOS, you’ll need to update your podfile with the correct iOS dependencies. Update your `Runner` and `NotificationServiceExtension` targets with the code below.

```
target 'Runner' do
  pod 'customer_io/fcm', :path => '.symlinks/plugins/customer_io/ios'
end

target 'NotificationServiceExtension' do
  pod 'customer_io_richpush/fcm', :path => '.symlinks/plugins/customer_io/ios'
end
```

### 4\. Update your push notification handler

In the previous version of the SDK, you had to initialize the SDK itself *and* the `MessagingPushFCM` package. You no longer need to initialize the SDK in your notification handler.

You’ll also notice that all the configuration settings for push are a part of the SDK initialization itself. You won’t set push settings in your notification handler anymore.

```
import UIKit
import Flutter
import CioMessagingPushFCM
import FirebaseMessaging
import FirebaseCore

@main
@objc class AppDelegate: FlutterAppDelegate {
    override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        GeneratedPluginRegistrant.register(with: self)
        
        // Depending on how you install Firebase, 
        // you may need to add functions to this file, like:
        // FirebaseApp.configure()
        // 
        // Read the official Firebase docs to install Firebase correctly! 

        Messaging.messaging().delegate = self
        
        MessagingPushFCM.initialize(
            withConfig: MessagingPushConfigBuilder()
                .build()
        )
        
        // This Sets a 3rd party push event handler for the app—rather than the Customer.io SDK and FlutterFire.
        // Setting the AppDelegate as the handler will internally use `flutter_local_notifications` to handle push events.
        UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate

        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }
    
    func application(application: UIApplication,
                    didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        Messaging.messaging().setAPNSToken(deviceToken, type: .unknown);
    }
    
    override func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
        MessagingPush.shared.application(application, didFailToRegisterForRemoteNotificationsWithError: error)
    }
}

extension AppDelegate: MessagingDelegate {
    func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
        MessagingPush.shared.messaging(messaging, didReceiveRegistrationToken: fcmToken)
    }
}
```

### 5\. Update your API calls

You’ll now make your calls from `.instance` methods. There are also a few crucial differences between the 1.x API and the 2.x API:

1.  `userId` replaces `identifier` in your `identify` call.
2.  `attributes` has changed to `traits` in `identify`, `setProfileAttributes`, and `setDeviceAttributes` methods.
3.  `properties` replaces `attributes` in `track`, `screen`, and any other [event](/journeys/people/events/) calls.

IdentifyTrackScreen

#### Identify

```
//new call
CustomerIO.instance.identify(userId: email, traits: {
  "name": user.displayName,
  "email": user.email,
  "age": user.age,
});

//old call
CustomerIO.identify(identifier: email, attributes: {
  "name": user.displayName,
  "email": user.email,
  "age": user.age,
});
```

#### Track

```
//new call
CustomerIO.instance.track(name: "Movie Watched", properties: {
  "movie_name": "The Incredibles",
  "watch_time_in_minutes": 102,
});

//old call
CustomerIO.track(name: "Movie Watched", attributes: {
  "movie_name": "The Incredibles",
  "watch_time_in_minutes": 102,
});
```

#### Screen

```
//new call
CustomerIO.instance.screen(title: "Settings", properties: {
  "source": "Dashboard",
  "is_logged_in": true,
});

//old call
CustomerIO.screen(name: "Settings", attributes: {
  "source": "Dashboard",
  "is_logged_in": true,
});
```

## Configuration Changes

As a part of this release, we’ve changed a few `CustomerIOConfig` configuration options when you initialize the SDK. The following table shows the changes to the configuration options.

Field

Type

Default

Description

`cdpApiKey`

string

Replaces `apiKey`; required to initialize the SDK and send data to Customer.io.

`migrationSiteId`

string

Replaces `siteId`; required if you’re updating from 2.x. This is the key representing your previous version of the SDK.

`trackApplicationLifeCycleEvents`

boolean

`true`

When true, the SDK automatically tracks application lifecycle events (like *Application Installed*).

`inAppConfig`

object

Replaces the former `enableInApp` option, providing a place to set in-app configuration options. For now, it takes a single property called `siteId`.

`pushConfig`

object

Optional push configuration settings directly in the `CustomerIOConfig`. For now, it only takes the `android.PushClickBehavior` setting. If you don’t set a `pushConfig`, we’ll use default push settings.

---

## Whats new > 3.5.0 upgrade

**Source:** /integrations/sdk/flutter/whats-new/3.5.0-upgrade

# Upgrade from 3.x to 3.5.0

Version 3.5.0 of the Customer.io Flutter SDK adds App Groups support for more reliable push delivery metric tracking on iOS. This update is additive—existing integrations work without modification.

## What changed?

Version 3.5.0 adds support for [App Groups](/integrations/sdk/flutter/push-notifications/app-groups/), which improves the reliability of push delivery metric tracking on iOS. This update is additive—your existing integration continues to work without changes. However, to take advantage of App Groups, you’ll need to update your Xcode project configuration and regenerate provisioning profiles if you use manual signing.

### Why app groups?

When you send a push notification, the SDK tracks delivery metrics in your Notification Service Extension (NSE). However, iOS can end the NSE before the tracking request completes, which means some delivery data may never reach Customer.io. App Groups provide shared storage between your main app and the NSE, so the SDK can save metrics and recover them on the next app launch.

## Upgrade process

1.  Update to version 3.5.0 or later of the Customer.io Flutter SDK.
2.  Follow the [App Groups setup instructions](/integrations/sdk/flutter/push-notifications/app-groups/) to configure your Xcode project and pass `.appGroupId()` to the SDK in your native Swift code.

No other code changes are needed.

---

## Whats new > 3.x upgrade

**Source:** /integrations/sdk/flutter/whats-new/3.x-upgrade

# 2.x -> 3.0.0

Version 3.x of the Customer.io Flutter SDK introduces Firebase wrapper support for FCM users that improves Firebase compatibility and simplifies push notification setup.

## What changed?

Version 3.x introduces a Firebase wrapper that improves compatibility with Firebase Cloud Messaging (FCM).

## What needs to change?

You need to add the `CioFirebaseWrapper` import to your Swift files that use `CioMessagingPushFCM`.

Add the Firebase wrapper import to your `AppDelegate.swift` file:

```
import UIKit
import Flutter
import CioMessagingPushFCM
import CioFirebaseWrapper  // Add this import for FCM users
import FirebaseMessaging
import FirebaseCore

@main
class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {}
```

## Troubleshooting

If you see build errors related to Firebase after upgrading:

1.  **Clean your build**: Run `cd ios && pod install && cd ..` then rebuild
2.  **Check imports**: Ensure you’ve added `import CioFirebaseWrapper` to all files that import `CioMessagingPushFCM`

---

## Whats new > 4.0.0 upgrade

**Source:** /integrations/sdk/flutter/whats-new/4.0.0-upgrade

# Upgrade from 3.x to 4.0.0

Version 4.0.0 of the Customer.io Flutter SDK adds Swift Package Manager (SPM) support for iOS.

## What changed?

Version 4.0.0 of the Flutter SDK adds Swift Package Manager (SPM) support as an alternative iOS dependency manager. SPM is Apple’s built-in package manager with native Xcode integration, so you don’t need to manage a separate Podfile for Customer.io dependencies. When SPM is enabled, the SDK is automatically resolved - no manual Xcode package setup needed for the main app.

CocoaPods continues to work. If you want to stay on CocoaPods, you don’t need to change anything.

## Upgrade process

Update to version 4.0.0 or later of the Customer.io Flutter SDK in your `pubspec.yaml`:

```
dependencies:
  customer_io: ^4.0.0
```

### If you use CocoaPods (no changes needed)

If you want to continue using CocoaPods, you don’t need to change anything. Your existing Podfile will continue to work. Just update the SDK version above and run `pod install --repo-update --project-directory=ios`.

### If you want to switch to SPM

#### Requirements

*   Flutter 3.24+
*   Xcode 16.0+
*   `firebase_core: ^3.5.0` or higher (we recommend at least `^4.6.0`)
*   `firebase_messaging: ^15.2.0` or higher (we recommend at least `^16.1.3`)

Older versions of `firebase_core` and `firebase_messaging` don’t support SPM and cause module conflicts.

#### Steps

1.  **Update Firebase dependencies** in your `pubspec.yaml` to SPM-compatible versions:
    
    ```
    dependencies:
      firebase_core: ^3.5.0  # we recommend at least ^4.6.0
      firebase_messaging: ^15.2.0  # we recommend at least ^16.1.3
    ```
    
2.  **Enable SPM (if not already enabled).** If you haven’t enabled Swift Package Manager in your Flutter project, you can do so globally or per-project:
    
    *   **Global**: Run `flutter config --enable-swift-package-manager`
        
    *   **Per-project**: Add the following to your `pubspec.yaml`:
        
        ```
        flutter:
          config:
            enable-swift-package-manager: true
        ```
        
3.  **Remove CIO pod lines and the NSE target from your Podfile.** Remove any `customer_io` pod lines and the `NotificationServiceExtension` target block:
    
    ```
    pod 'customer_io/fcm', :path => ...
    pod 'customer_io/location', :path => ...
    pod 'customer_io_richpush/fcm', :path => ...
    ```
    
    With SPM enabled, Flutter’s pod helper automatically skips SPM-handled plugins. The core SDK, in-app messaging, and push notifications modules (`DataPipelines`, `MessagingInApp`, `MessagingPushFCM`) are automatically included via SPM - no additional setup needed.
    
    Using the Location module?
    
    If you use the Location module, make sure `customerio_location_enabled=true` is set in your `android/gradle.properties`. This flag controls Location inclusion on both Android and iOS when using SPM. If the flag can’t be read (e.g. in add-to-app modules or some CI setups), you can use the `CIO_LOCATION` environment variable as a fallback: `CIO_LOCATION=true flutter build ios`. See [Location tracking](/integrations/sdk/flutter/tracking/location/) for details.
    
4.  **Remove the old CocoaPods framework from the NSE target** in Xcode. Go to your **NotificationServiceExtension** target > **General** > **Frameworks, Libraries, and Embedded Content** and remove `Pods_NotificationServiceExtension.framework`.
    
5.  **Add the SPM framework to the NSE target.** In the same **Frameworks and Libraries** section, click **+** and add **FlutterGeneratedPluginSwiftPackage**.
    
     ![Add FlutterGeneratedPluginSwiftPackage to the NotificationServiceExtension target](https://customer.io/images/xcode-nse-spm-framework.png)
    
6.  **Add `import UserNotifications`** to the top of your `NotificationService.swift`:
    
    ```
    import CioMessagingPushFCM
    import UserNotifications
    ```
    
    SPM doesn’t transitively import system frameworks like CocoaPods does, so you must add the import.
    
7.  **Clean and rebuild:**
    
    ```
    cd ios && rm -rf Pods Podfile.lock
    cd .. && flutter clean && flutter pub get
    flutter build ios
    ```
    

## Troubleshooting

If you run into issues during migration, see [SPM troubleshooting](/integrations/sdk/flutter/getting-started/troubleshooting/#spm-issues) for solutions to common problems like module conflicts, deployment target errors, and CI build failures.

---

## Whats new > Changelog

**Source:** /integrations/sdk/flutter/whats-new/changelog

# Flutter Changelog

Check out the release history for the Flutter SDK. Stable releases have been tested thoroughly and are ready for use in your production apps.

Need to upgrade?

Select your current version to see everything that changed between it and the latest Flutter release.

Select a version…4.4.04.3.14.3.04.2.14.2.04.1.34.1.24.1.14.1.04.0.24.0.14.0.03.5.03.4.03.3.03.2.23.2.13.2.03.1.13.1.03.0.13.0.02.9.02.8.02.7.02.6.12.6.02.5.02.4.32.4.22.4.12.4.02.3.12.3.02.2.02.1.82.1.72.1.62.1.52.1.42.1.32.1.22.1.12.1.02.0.01.5.21.5.11.5.01.4.01.3.21.3.11.3.01.2.71.2.61.2.51.2.41.2.31.2.21.2.11.2.01.1.21.1.11.1.01.0.01.0.0-beta.31.0.0-beta.21.0.0-beta.11.0.0-alpha.91.0.0-alpha.81.0.0-alpha.71.0.0-alpha.61.0.0-alpha.51.0.0-alpha.41.0.0-alpha.31.0.0-alpha.21.0.0-alpha.10.0.1-alpha.30.0.1-alpha.2

## 4.x Releases

* * *

### 4.4.0

August 26, 2026 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/4.4.0)

#### Features

*   route SDK deep links through Flutter scenes ([#399](https://github.com/customerio/customerio-flutter/issues/399)) ([6dbd5f3](https://github.com/customerio/customerio-flutter/commit/6dbd5f3bbbf5685c719c2bf3d678c52c2cc47630))

### 4.3.1

August 21, 2026 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/4.3.1)

#### Bug Fixes

*   restore Flutter package publishing ([#398](https://github.com/customerio/customerio-flutter/issues/398)) ([545b47b](https://github.com/customerio/customerio-flutter/commit/545b47b9e82af21d8642ba2d7b9f4a9e51629653))

### 4.3.0

August 21, 2026 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/4.3.0)

#### Features

*   add Flutter UIScene lifecycle adapter (MBL-2277) ([#394](https://github.com/customerio/customerio-flutter/issues/394)) ([febd7ed](https://github.com/customerio/customerio-flutter/commit/febd7ed72c309fbab70ca61ac45c8f2d5120de72))

### 4.2.1

August 7, 2026 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/4.2.1)

#### Bug Fixes

*   update native SDKs — iOS 4.7.2 (geofence enter/exit reliability and in-app page route rule matching) and Android 4.20.1 (geofence enter/exit event accuracy) ([#379](https://github.com/customerio/customerio-flutter/issues/379)) ([5db379f](https://github.com/customerio/customerio-flutter/commit/5db379f0cdbb7bd5b9b070f081b3a245f86929f5))

### 4.2.0

July 30, 2026 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/4.2.0)

#### Features

*   Enable Live Notifications feature ([#373](https://github.com/customerio/customerio-flutter/issues/373)) ([1d395a2](https://github.com/customerio/customerio-flutter/commit/1d395a287624906003f9f16c94d71ebc376778a5))
*   **geofence:** add support for geofencing feature ([#371](https://github.com/customerio/customerio-flutter/issues/371)) ([f961466](https://github.com/customerio/customerio-flutter/commit/f9614661ff443108c5b5ea382dbfd898221559e6))
*   **inbox:** Flutter Visual Notification Inbox ([#375](https://github.com/customerio/customerio-flutter/issues/375)) ([79380f7](https://github.com/customerio/customerio-flutter/commit/79380f7eaab514812f54463d60903aac3ee894d3))

### 4.1.3

July 24, 2026 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/4.1.3)

### 4.1.2

June 23, 2026 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/4.1.2)

#### Bug Fixes

*   update native SDKs — iOS 4.5.3 (resolve duplicate event delivery bugs) and Android 4.18.2 (scope in-app polling to process lifecycle) ([#352](https://github.com/customerio/customerio-flutter/issues/352)) ([852d9bf](https://github.com/customerio/customerio-flutter/commit/852d9bfb12c27abdbeb2dad94d1e26439576787b))

### 4.1.1

June 16, 2026 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/4.1.1)

#### Bug Fixes

*   release native SDK update (iOS 4.5.1, Android 4.18.1) ([#349](https://github.com/customerio/customerio-flutter/issues/349)) ([7216e8d](https://github.com/customerio/customerio-flutter/commit/7216e8dad174d8576b69c03750739354a24f653d))
    *   **Android `4.18.1`** — Harden push notification activity and in-app message WebView ([release notes](https://github.com/customerio/customerio-android/releases/tag/4.18.1))
    *   **iOS `4.5.1`** — Internal test-isolation refactor; no runtime impact ([release notes](https://github.com/customerio/customerio-ios/releases/tag/4.5.1))

### 4.1.0

June 5, 2026 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/4.1.0)

#### Features

*   update native SDKs to iOS 4.5.0 and Android 4.18.0 ([#346](https://github.com/customerio/customerio-flutter/issues/346)) ([b17dcb0](https://github.com/customerio/customerio-flutter/commit/b17dcb0f3e4a23f2705b02d173fc2123571a152f))

### 4.0.2

May 21, 2026 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/4.0.2)

#### Bug Fixes

*   update Customer.io native SDKs (iOS 4.4.3, Android 4.17.1) ([#342](https://github.com/customerio/customerio-flutter/issues/342)) ([ad1fece](https://github.com/customerio/customerio-flutter/commit/ad1fecec920dc774f63cf50dbada57a590312184))

### 4.0.1

April 24, 2026 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/4.0.1)

#### Bug Fixes

*   Fix in-app messages not displaying when app returns from background when SSE enabled ([#334](https://github.com/customerio/customerio-flutter/issues/334))

### 4.0.0

April 10, 2026 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/4.0.0)

#### ⚠ BREAKING CHANGES

*   Added support for Swift Package Manager (SPM) in Flutter SDK. No changes for apps using CocoaPods, apps using SPM should refer to the updated documentation for setup instructions. (#330)

#### Features

*   Added support for Swift Package Manager (SPM) ([#330](https://github.com/customerio/customerio-flutter/issues/330)) ([8abff81](https://github.com/customerio/customerio-flutter/commit/8abff815225379dab5e350cbb5b6d33af213512b))

## 3.x Releases

* * *

### 3.5.0

April 2, 2026 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/3.5.0)

#### Features

*   Push delivery reslience improvement ([#321](https://github.com/customerio/customerio-flutter/issues/321)) ([7c23a0a](https://github.com/customerio/customerio-flutter/commit/7c23a0aaa9b647cf06db2e592bd8debc73edeb96))

### 3.4.0

March 11, 2026 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/3.4.0)

#### Features

*   Location enrichment ([#316](https://github.com/customerio/customerio-flutter/issues/316)) ([1b36d69](https://github.com/customerio/customerio-flutter/commit/1b36d6906599a15f87448859e943e8df049687e4))

### 3.3.0

February 21, 2026 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/3.3.0)

#### Features

*   Added support for Notification Inbox ([#313](https://github.com/customerio/customerio-flutter/issues/313)) ([b1e732b](https://github.com/customerio/customerio-flutter/commit/b1e732b7606e2f95e54d9a50f00cbbeb72b119ea))

### 3.2.2

February 16, 2026 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/3.2.2)

#### Bug Fixes

*   Downgrade okhttp version to 4.x ([#307](https://github.com/customerio/customerio-flutter/issues/307)) ([b0dff34](https://github.com/customerio/customerio-flutter/commit/b0dff34c2e5acd29f7b373820d667bafc8c63fb3))

### 3.2.1

February 5, 2026 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/3.2.1)

#### Bug Fixes

*   Remove outdated sonatype snapshot dependency ([#303](https://github.com/customerio/customerio-flutter/issues/303)) ([02309bf](https://github.com/customerio/customerio-flutter/commit/02309bf36a334663a9c99d86f1f4a3f1bcaa9ca9))

### 3.2.0

January 14, 2026 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/3.2.0)

#### Features

*   In-app messages now support SSE (Server-Sent Events) as an alternative to polling, reducing latency and improving message delivery efficiency ([#298](https://github.com/customerio/customerio-flutter/issues/298)) ([59ab310](https://github.com/customerio/customerio-flutter/commit/59ab3102e6b78f404e59bc937fa0fa783705d1bf))

### 3.1.1

December 16, 2025 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/3.1.1)

#### Bug Fixes

*   Bump Android compileSdk to 36 for AndroidX compatibility ([#297](https://github.com/customerio/customerio-flutter/issues/297)) ([5b8f972](https://github.com/customerio/customerio-flutter/commit/5b8f9725b0a8df3d99e4b53833a0d1c2f1d4b6fb))

### 3.1.0

October 30, 2025 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/3.1.0)

#### Features

*   Updated the SDK and dependencies for Android 16 compatibility, including minor updates to better support newer OS restrictions and behavior changes. ([#293](https://github.com/customerio/customerio-flutter/issues/293)) ([2b235de](https://github.com/customerio/customerio-flutter/commit/2b235de04cea8b51682e2acb8021c0830686aa8f))

#### ⚠️ Notes

*   Apps now may need to update their `compileSdk` version to `36` and Gradle version to at least `8.9.3` to ensure compatibility with updated dependencies and to successfully build against Android 16.

### 3.0.1

October 24, 2025 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/3.0.1)

#### Bug Fixes

*   prevent message type crash in release builds ([#291](https://github.com/customerio/customerio-flutter/issues/291)) ([44ff921](https://github.com/customerio/customerio-flutter/commit/44ff92167b4c26b42e018b1288f58e1631e6a1ce))

### 3.0.0

October 17, 2025 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/3.0.0)

#### ⚠ BREAKING CHANGES

*   Add CioFirebaseWrapper to pull in Firebase specific services (#290)

#### Features

*   Add CioFirebaseWrapper to pull in Firebase specific services ([#290](https://github.com/customerio/customerio-flutter/issues/290)) ([444becb](https://github.com/customerio/customerio-flutter/commit/444becbb27ac9e49bb2f4d81dd0f75a3d50b7ad1))

## 2.x Releases

* * *

### 2.9.0

October 8, 2025 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/2.9.0)

#### Features

*   You can now send banners, modals, pop-ups, and surveys to anonymous visitors —no ID or email required. ([#289](https://github.com/customerio/customerio-flutter/issues/289)) ([47f1918](https://github.com/customerio/customerio-flutter/commit/47f1918562a498117952bd77163398f9ec529133))

### 2.8.0

October 7, 2025 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/2.8.0)

#### Features

*   Improve push notificaiton delivery receipts delay ([#287](https://github.com/customerio/customerio-flutter/issues/287)) ([4503048](https://github.com/customerio/customerio-flutter/commit/4503048d98613664888b9f60e1a4ca75690d9270))

### 2.7.0

October 3, 2025 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/2.7.0)

#### Breaking Features

*   Support for Kotlin 2+ is added (#591) (b5f94ff)

but this also means apps needs to have the following minimum requirement

*   Gradle: 8.0 or later
*   Android Gradle Plugin (AGP): 8.0 or later (8.2+ recommended)
*   Kotlin: 1.9.20 or later (2.0+ required if using Kotlin Multiplatform or K2-specific features)

#### Features

*   Upgrade Kotlin and AGP versions ([#284](https://github.com/customerio/customerio-flutter/issues/284)) ([ed9da81](https://github.com/customerio/customerio-flutter/commit/ed9da81ad05500e07224391e696f725cc75d4b76))
*   Added support for queue sticky sessions from latest Android native SDK (customerio/customerio-android#598)
*   Align public API with other CIO SDK platforms from latest Android native SDK (customerio/customerio-android#600)

#### Bug Fixes

*   Resolved a crash when dismissing in app messages using back press during initial loading phase. Users can now safely navigate away from messages without encountering unexpected app crashes. from latest Android native SDK (customerio/customerio-android#608)

### 2.6.1

September 29, 2025 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/2.6.1)

#### Bug Fixes

*   Upgrade Android compileSdk to 34 ([#282](https://github.com/customerio/customerio-flutter/issues/282)) ([9a29530](https://github.com/customerio/customerio-flutter/commit/9a29530c9fb0646926429205fa7b875bc3701cdc))

### 2.6.0

September 16, 2025 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/2.6.0)

#### Improvements

*   Updated Native iOS SDK from `3.11.0` to `3.13.1` which includes following updates: ([#280](https://github.com/customerio/customerio-flutter/issues/280)) ([70c78cc](https://github.com/customerio/customerio-flutter/commit/70c78cc7c93751f5404d580d1a466416f4466dc4))
    *   Added support for queue sticky sessions
    *   Align public API with other CIO SDK platforms
    *   Fixed build issues on Xcode 26 beta that only affected apps using CocoaPods
    *   Fixed an issue where custom scheme URLs were not opening when using FCM with `CioAppDelegateWrapper`

### 2.5.0

August 27, 2025 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/2.5.0)

#### Features

*   Align public API with other CIO SDK platforms ([#271](https://github.com/customerio/customerio-flutter/issues/271)) ([b559742](https://github.com/customerio/customerio-flutter/commit/b559742375205a082fca820d3aa1923ef94b1741))

### 2.4.3

August 11, 2025 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/2.4.3)

#### Bug Fixes

*   Fixed an issue where inline message view occupied `1dp` of space even when no content was available. With this fix, the view no longer takes up any visual space when content is unavailable and only becomes visible when there is content to display. ([#259](https://github.com/customerio/customerio-flutter/issues/259)) ([b78cffd](https://github.com/customerio/customerio-flutter/commit/b78cffda57f2940b321a7c0e2afab9627a78ffae))

### 2.4.2

July 24, 2025 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/2.4.2)

#### Fixes and Tooling update

*   Support for FCM 12.x is added which requires a minimum iOS deployment target of 15. If you're using FCM module, ensure your deployment target and tooling are up to date. Or lock Firebase to 11.x to avoid compatibility issues
*   Fixes a crash when build attributes from device are nullable (iOS 3.11.0, Android 4.7.1) ([#253](https://github.com/customerio/customerio-flutter/issues/253)) ([9e66b4b](https://github.com/customerio/customerio-flutter/commit/9e66b4beee2ebbbe3bbf7e79e83011255fd91349))

### 2.4.1

July 21, 2025 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/2.4.1)

#### Bug Fixes

*   Fixed build issue on iOS by adding a default implementation for an internal SPI method in `DeepLinkUtil`, preventing conformance errors with `BUILD_LIBRARY_FOR_DISTRIBUTION = YES` ([#249](https://github.com/customerio/customerio-flutter/issues/249)) ([7313421](https://github.com/customerio/customerio-flutter/commit/731342157082e76ae846c1964ad008f53e3c1834))

### 2.4.0

July 17, 2025 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/2.4.0)

#### Features

*   Add ability to configure messaging channels for local notifications ([#247](https://github.com/customerio/customerio-flutter/issues/247)) ([0541919](https://github.com/customerio/customerio-flutter/commit/05419193144bd3cebcdbd732fb14091ead6fa86a))

### 2.3.1

July 9, 2025 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/2.3.1)

#### Bug Fixes

*   Preserve numeric types when doing sanitization for JSON ([#246](https://github.com/customerio/customerio-flutter/issues/246)) ([ea94c07](https://github.com/customerio/customerio-flutter/commit/ea94c078a5003f0cca1b794be04d783291f2b06e))

### 2.3.0

June 26, 2025 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/2.3.0)

#### Features

*   Added support for inline in-app messages. Inline in-app messages act like a part of the content on your page. They let you dynamically populate parts of your app and talk to your customers without interrupting their experience. ([39e7b87](https://github.com/customerio/customerio-flutter/commit/39e7b8751210056646f976aebea2c377c59156de))

### 2.2.0

June 2, 2025 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/2.2.0)

#### Features

*   Add support for iOS no-swizzling approach and improve example app setup
*   Fixed an issue where the SDK enforced a strict version of firebase messaging, preventing integration with newer versions.
*   Fixes the bug where multi screen in-app messages might dismiss earlier than intended
*   ([#226](https://github.com/customerio/customerio-flutter/issues/226)) ([2f4c0a2](https://github.com/customerio/customerio-flutter/commit/2f4c0a2064a6006829df5b0ee68627f501c62822))

### 2.1.8

May 30, 2025 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/2.1.8)

#### Bug Fixes

*   Fixed a crash that occurred only when the app was restored by Android OS from background after being removed from memory with a modal in-app message active. ([#225](https://github.com/customerio/customerio-flutter/issues/225)) ([b521461](https://github.com/customerio/customerio-flutter/commit/b521461a823e42d4ff040b83e7c37d957c83b839))

### 2.1.7

May 22, 2025 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/2.1.7)

#### Bug Fixes

*   Fixed an issue where SDK calls returning no result (i.e. `void`) were incorrectly logged as errors for unsupported values. The fix removes unnecessary error log without affecting any functionality or behavior. ([#224](https://github.com/customerio/customerio-flutter/issues/224)) ([2022c27](https://github.com/customerio/customerio-flutter/commit/2022c27609462461596ddc50076a134f38f514fb))

### 2.1.6

April 28, 2025 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/2.1.6)

Updates the iOS native dependency from `3.8.1` to `3.8.2` and Android native dependency from `4.5.6` to `4.5.8` which includes the following improvements.([#221](https://github.com/customerio/customerio-flutter/issues/221)) ([56418f2](https://github.com/customerio/customerio-flutter/commit/56418f292b377fbf5f05f14143be1740e7bce31e))

#### Bug Fixes

*   \[iOS\] Region codes are now handled more flexibly—if we can't recognize one, we use a default instead of blocking SDK startup. ([https://github.com/customerio/customerio-ios/pull/879](https://github.com/customerio/customerio-ios/pull/879))
*   \[iOS\] Set a safe upper limit for the Firebase (FCM) version in CocoaPods to avoid build issues with newer versions.([https://github.com/customerio/customerio-ios/pull/880](https://github.com/customerio/customerio-ios/pull/880))
*   \[Android\]Fixed an issue where a freshly generated Firebase token was being deleted by mistake. Now, only outdated tokens are removed. ([https://github.com/customerio/customerio-android/issues/517](https://github.com/customerio/customerio-android/issues/517)) ([ccb9de2](https://github.com/customerio/customerio-android/commit/ccb9de27e7ad38e2569a5a9d91d9fec2cd5293ce))
*   \[Android\] Custom events triggered via the plugin now follow the same timing and lifecycle as other SDK events. ([https://github.com/customerio/customerio-android/issues/521](https://github.com/customerio/customerio-android/issues/521)) ([1c0b7a6](https://github.com/customerio/customerio-android/commit/1c0b7a6c7bc3b14820b9c6bab3168025206cbfbd))

### 2.1.5

April 14, 2025 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/2.1.5)

#### Bug Fixes

*   Better support for user data with missing fields—SDK now handles null values in nested attributes or traits gracefully. ([#217](https://github.com/customerio/customerio-flutter/issues/217)) ([0d5928b](https://github.com/customerio/customerio-flutter/commit/0d5928b27292def1e427becb165d64880bb46883))

### 2.1.4

April 3, 2025 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/2.1.4)

Updates the iOS native dependency from `3.8.0` to `3.8.1` and Android native dependency from `4.5.3` to `4.5.5` which includes the following improvements.

#### Bug Fixes

*   \[iOS\] Fixed an issue where the “Application Installed” event was incorrectly triggered on every app launch instead of only after the initial installation.
*   \[Android\] Resolved syncing issues for events stored while in battery saver (offline) mode ([https://github.com/customerio/customerio-android/issues/498](https://github.com/customerio/customerio-android/issues/498)) ([6f3d16f](https://github.com/customerio/customerio-android/commit/6f3d16fe01a675cfa522099230baf03650cf9c42))
*   \[Android\] Fixed the sequencing of screen tracking events for in-app messaging current screen state ([https://github.com/customerio/customerio-android/issues/500](https://github.com/customerio/customerio-android/issues/500)) ([6877daf](https://github.com/customerio/customerio-android/commit/6877daf98235ce9c96a2ce4932f188efb2c33a71))

### 2.1.3

February 27, 2025 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/2.1.3)

#### Bug Fixes

*   Incorrectly scrolling content for in-app modal messages positioned top/bottom ([#208](https://github.com/customerio/customerio-flutter/issues/208)) ([5bdf0eb](https://github.com/customerio/customerio-flutter/commit/5bdf0ebae6a5c2f0f34589d80c77212557dfd033))

### 2.1.2

February 21, 2025 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/2.1.2)

#### Bug Fixes

*   Android 14 introduced strict rules for when apps are in the killed state, impacting push delivery tracking. This release fixes that.([#204](https://github.com/customerio/customerio-flutter/issues/204)) ([8b60a4e](https://github.com/customerio/customerio-flutter/commit/8b60a4ee1d5357ee871fcf03684b307a117350fa))

### 2.1.1

January 9, 2025 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/2.1.1)

Updates the iOS native dependency from `3.7.1` to `3.7.2` and Android native dependency from `4.5.0` to `4.5.2` which includes the following improvements.

#### Bug Fixes

*   \[Android\] Fixes the bug where device update/delete events would not migrate automatically when migrating from v3 to v4 ([https://github.com/customerio/customerio-android/issues/481](https://github.com/customerio/customerio-android/issues/481))
*   \[Android & iOS\] Fixes in-app messages overlay background color being ignored from in-app message payload ([https://github.com/customerio/customerio-android/issues/485](https://github.com/customerio/customerio-android/issues/485)) ([https://github.com/customerio/customerio-ios/issues/843](https://github.com/customerio/customerio-ios/issues/843))

### 2.1.0

January 6, 2025 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/2.1.0)

#### Features

*   Added ability to disable forwarding screen events to destinations/servers. Apps can still send screen events for local processing and use them for page rules in in-app messages by updating SDK configuration during initialization. ([#190](https://github.com/customerio/customerio-flutter/issues/190)) ([b4a4fed](https://github.com/customerio/customerio-flutter/commit/b4a4feddb943972f635826d55d31a4cb14904210))

### 2.0.0

December 10, 2024 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/2.0.0)

#### ⚠ BREAKING CHANGES

*   Data Pipelines Support: Sending your mobile data into our customer data platform (CDP) helps you support Journeys and the rest of your martech stack—analytics, data warehouses, CRMs, and more. (#187)

> ***NOTE:*** Please follow the [migration guide](https://docs.customer.io/sdk/flutter/whats-new/2.x-upgrade/) for a seamless upgrade to this version.

#### Features

*   Anonymous tracking: You can send anonymous events, and we’ll reconcile anonymous activity with your users when you identify them.
*   Built-in lifecycle events: the SDK now automatically captures events like “Application Installed” and “Application Updated” for you, so you better understand your users’ behaviors in your app.
*   New device data: The SDK captures complete device-level context, such as your audience’s screen dimensions, device names, and more.

#### Improvements

*   Resolves an issue where the user agent for events was labeled as coming from the iOS SDK instead of the Flutter SDK.

## 1.x Releases

* * *

### 1.5.2

September 6, 2024 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/1.5.2)

Updates Android Native dependency from **3.11.0** to **3.11.2** which includes following improvements.

#### Features

*   Messages created in the [new drag and drop in-app editor](https://customer.io/docs/release-notes/2024-07-15-in-app-visual-editor/) will load much faster and don’t require pre-fetching assets. If you are using the older in-app editor, the initial in-app message may experience a delay. We highly recommend upgrading to the new in-app editor to avoid this issue and ensure optimal performance. \[[#139](https://github.com/customerio/customerio-flutter/issues/139)\] ([b10cc75](https://github.com/customerio/customerio-flutter/commit/b10cc751d6f220e7c7ade1c189bba28ee16b68ff))

#### Bug Fixes

*   Fixed crash that could occur on some Android devices during asset pre-fetching step in certain situations. \[[#139](https://github.com/customerio/customerio-flutter/issues/139)\] ([b10cc75](https://github.com/customerio/customerio-flutter/commit/b10cc751d6f220e7c7ade1c189bba28ee16b68ff))
*   Resolved an issue where calling `CustomerIO.MessagingInApp().dismissMessage()` explicitly would print crash logs on Android. The method can now be called safely without unnecessary logs. \[[#142](https://github.com/customerio/customerio-flutter/pull/142)\] ([1635acf](https://github.com/customerio/customerio-flutter/commit/1635acfba88fcf74462072294a4d3e70f069e119))

### 1.5.1

August 1, 2024 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/1.5.1)

#### Bug Fixes

*   Resolved an issue where the native iOS code communicated to the Flutter code on a non-platform thread. This change ensures that platform channel messages are sent on the correct platform thread, preventing potential data loss or crashes. ([#134](https://github.com/customerio/customerio-flutter/issues/134)) ([bc8704b](https://github.com/customerio/customerio-flutter/commit/bc8704b03891372566f3450ed02a082ecb0ae4a9))

### 1.5.0

July 2, 2024 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/1.5.0)

#### Features

*   When using page rules and when an in-app messages need a second to load, the in-app message might appear after a user navigates to another page. We made changes to ensure the page-rules are checked after the message is loaded and immediately before it’s displayed in order to resolve this issue.([#131](https://github.com/customerio/customerio-flutter/issues/131)) ([a563055](https://github.com/customerio/customerio-flutter/commit/a5630555e34668a10c299a1b22f59afcc46217e9))

### 1.4.0

April 18, 2024 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/1.4.0)

#### Features

*   support for android gradle plugin 8 ([#117](https://github.com/customerio/customerio-flutter/issues/117)) ([48b4e06](https://github.com/customerio/customerio-flutter/commit/48b4e06ae85ef7bd58e963e89d67d23d7191d5aa))

### 1.3.2

April 8, 2024 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/1.3.2)

#### Bug Fixes

*   added proguard rules for R8 strict mode ([#116](https://github.com/customerio/customerio-flutter/issues/116)) ([9946fe7](https://github.com/customerio/customerio-flutter/commit/9946fe73938ae25caf4beac710855feeaa2e4dba))

### 1.3.1

November 14, 2023 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/1.3.1)

#### Bug Fixes

*   improve android push click behavior ([#89](https://github.com/customerio/customerio-flutter/issues/89)) ([62b9e61](https://github.com/customerio/customerio-flutter/commit/62b9e615cbe0c145319d4a5ceb8ab44823b6adb6))

### 1.3.0

October 25, 2023 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/1.3.0)

#### Features

*   in-app message persistence ([#97](https://github.com/customerio/customerio-flutter/issues/97)) ([71d85cc](https://github.com/customerio/customerio-flutter/commit/71d85cc4ce25a37399a68afda975abea19bfccc1))

### 1.2.7

October 18, 2023 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/1.2.7)

#### Bug Fixes

*   in-app crash for no browser ([#94](https://github.com/customerio/customerio-flutter/issues/94)) ([8b859ed](https://github.com/customerio/customerio-flutter/commit/8b859eddf383c1a195767eb87cf6513bb81ac44c))

### 1.2.6

September 28, 2023 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/1.2.6)

#### Bug Fixes

*   stack-overflow caused by BQ recursion ([#90](https://github.com/customerio/customerio-flutter/issues/90)) ([ebc7511](https://github.com/customerio/customerio-flutter/commit/ebc7511d19409d7524f5aa8dd5446d85dc14ecfb))

### 1.2.5

September 7, 2023 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/1.2.5)

#### Bug Fixes

*   in-app concurrency issue android ([#73](https://github.com/customerio/customerio-flutter/issues/73)) ([93332a4](https://github.com/customerio/customerio-flutter/commit/93332a44f0865534b414626a9f177efbb979f311))

### 1.2.4

July 25, 2023 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/1.2.4)

#### Bug Fixes

*   in-app messages not displaying for release builds on Android ([#65](https://github.com/customerio/customerio-flutter/issues/65)) ([1d742c2](https://github.com/customerio/customerio-flutter/commit/1d742c2975fd200df45a7e6c81c54eead0302a9c))

### 1.2.3

July 14, 2023 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/1.2.3)

#### Bug Fixes

*   hardcode android native SDK version ([#61](https://github.com/customerio/customerio-flutter/issues/61)) ([587f559](https://github.com/customerio/customerio-flutter/commit/587f559fbba4864d17f0506875474ff705866189))

### 1.2.2

June 27, 2023 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/1.2.2)

#### Bug Fixes

*   iOS crash on forced unwrapping ([#59](https://github.com/customerio/customerio-flutter/issues/59)) ([f514174](https://github.com/customerio/customerio-flutter/commit/f5141742c49e876f40034d366c7122d519ee897e))

### 1.2.1

June 6, 2023 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/1.2.1)

#### Bug Fixes

*   updated module name from common to CioInternalCommon ([#55](https://github.com/customerio/customerio-flutter/issues/55)) ([d81f8df](https://github.com/customerio/customerio-flutter/commit/d81f8df2f3c686f197625c77900af0862211b8e1))

### 1.2.0

June 1, 2023 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/1.2.0)

#### Features

*   in-app dismiss support ([#51](https://github.com/customerio/customerio-flutter/issues/51)) ([c4d21f2](https://github.com/customerio/customerio-flutter/commit/c4d21f2c294ee37ebfc8f644bc6580cd44556544))

### 1.1.2

May 3, 2023 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/1.1.2)

#### Bug Fixes

*   autoupdate to latest major version of iOS SDK ([#40](https://github.com/customerio/customerio-flutter/issues/40)) ([974a342](https://github.com/customerio/customerio-flutter/commit/974a3422544e712ae52fc7ca1a8318601d0859c2))

### 1.1.1

April 26, 2023 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/1.1.1)

#### Bug Fixes

*   missing opened metric on android 12 and above ([#43](https://github.com/customerio/customerio-flutter/issues/43)) ([1a61e0e](https://github.com/customerio/customerio-flutter/commit/1a61e0e587bd0315122db2783b76930fa372c589))

### 1.1.0

April 26, 2023 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/1.1.0)

#### Features

*   process push notifications received outside CIO SDK ([#38](https://github.com/customerio/customerio-flutter/issues/38)) ([7b5cb7e](https://github.com/customerio/customerio-flutter/commit/7b5cb7e2aab1f7d3d9a521d5a58b9d0f3eab5177))

### 1.0.0

April 3, 2023 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/1.0.0)

#### Features

*   added missing methods ([#17](https://github.com/customerio/customerio-flutter/issues/17)) ([73f29e6](https://github.com/customerio/customerio-flutter/commit/73f29e64cb58c24f97f3654c5519e22e5255c507))
*   added SDK config ([#1](https://github.com/customerio/customerio-flutter/issues/1)) ([e8ed7dd](https://github.com/customerio/customerio-flutter/commit/e8ed7ddef985895840a4e406c1a5bb35250c7096))
*   tracking and in-app added ([#2](https://github.com/customerio/customerio-flutter/issues/2)) ([c23f2d9](https://github.com/customerio/customerio-flutter/commit/c23f2d936b801692618c5938ab3d32183345fbbe))

#### Bug Fixes

*   add test coverage and refactored scripts ([#34](https://github.com/customerio/customerio-flutter/issues/34)) ([f7f2387](https://github.com/customerio/customerio-flutter/commit/f7f2387941a6416411b26061420ccfb98ed0db72))
*   formatting issues ([d67fa7e](https://github.com/customerio/customerio-flutter/commit/d67fa7eea264d20e1b6314277bef926150625f4e))
*   in-app remove gist org id ([#19](https://github.com/customerio/customerio-flutter/issues/19)) ([ce4cc9e](https://github.com/customerio/customerio-flutter/commit/ce4cc9e4dd596ba2ff47e02199096154eac8ec06))
*   missing methods and extra dependency ([2c5deca](https://github.com/customerio/customerio-flutter/commit/2c5decac44288e6fba439c1a5669032a0282c69c))
*   obj-c bindings issue ([0dbe4ef](https://github.com/customerio/customerio-flutter/commit/0dbe4ef4a893df10d5bf060b6635240699357fa1))
*   plugin version in user-agent ([a10e482](https://github.com/customerio/customerio-flutter/commit/a10e482de83bb4f648408f99a2bed1cade6561c7))
*   release script typo ([2a8b7ae](https://github.com/customerio/customerio-flutter/commit/2a8b7aea96d74cd095c122cede2f7c7ff3dfbd89))
*   typo fixed ([#9](https://github.com/customerio/customerio-flutter/issues/9)) ([a5107df](https://github.com/customerio/customerio-flutter/commit/a5107dfb566561d9b8d3155f0a8a87096de78ee2)), closes [#7](https://github.com/customerio/customerio-flutter/issues/7) [#8](https://github.com/customerio/customerio-flutter/issues/8)
*   updated android dependency to auto update ([#24](https://github.com/customerio/customerio-flutter/issues/24)) ([040cef2](https://github.com/customerio/customerio-flutter/commit/040cef205821c9bb1913a378dfffa356a3b25650))
*   updated icon and typo ([57c6eef](https://github.com/customerio/customerio-flutter/commit/57c6eefb7d77bc88972450d75e74f1982abb8b16))

### 1.0.0-beta.3

March 31, 2023 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/1.0.0-beta.3)

#### Bug Fixes

*   release script typo ([2a8b7ae](https://github.com/customerio/customerio-flutter/commit/2a8b7aea96d74cd095c122cede2f7c7ff3dfbd89))

### 1.0.0-beta.2

March 31, 2023 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/1.0.0-beta.2)

#### Bug Fixes

*   add test coverage and refactored scripts ([#34](https://github.com/customerio/customerio-flutter/issues/34)) ([f7f2387](https://github.com/customerio/customerio-flutter/commit/f7f2387941a6416411b26061420ccfb98ed0db72))

### 1.0.0-beta.1

March 7, 2023 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/1.0.0-beta.1)

#### Features

*   added missing methods ([#17](https://github.com/customerio/customerio-flutter/issues/17)) ([73f29e6](https://github.com/customerio/customerio-flutter/commit/73f29e64cb58c24f97f3654c5519e22e5255c507))
*   added SDK config ([#1](https://github.com/customerio/customerio-flutter/issues/1)) ([e8ed7dd](https://github.com/customerio/customerio-flutter/commit/e8ed7ddef985895840a4e406c1a5bb35250c7096))
*   tracking and in-app added ([#2](https://github.com/customerio/customerio-flutter/issues/2)) ([c23f2d9](https://github.com/customerio/customerio-flutter/commit/c23f2d936b801692618c5938ab3d32183345fbbe))

#### Bug Fixes

*   formatting issues ([d67fa7e](https://github.com/customerio/customerio-flutter/commit/d67fa7eea264d20e1b6314277bef926150625f4e))
*   in-app remove gist org id ([#19](https://github.com/customerio/customerio-flutter/issues/19)) ([ce4cc9e](https://github.com/customerio/customerio-flutter/commit/ce4cc9e4dd596ba2ff47e02199096154eac8ec06))
*   missing methods and extra dependency ([2c5deca](https://github.com/customerio/customerio-flutter/commit/2c5decac44288e6fba439c1a5669032a0282c69c))
*   obj-c bindings issue ([0dbe4ef](https://github.com/customerio/customerio-flutter/commit/0dbe4ef4a893df10d5bf060b6635240699357fa1))
*   plugin version in user-agent ([a10e482](https://github.com/customerio/customerio-flutter/commit/a10e482de83bb4f648408f99a2bed1cade6561c7))
*   typo fixed ([#9](https://github.com/customerio/customerio-flutter/issues/9)) ([a5107df](https://github.com/customerio/customerio-flutter/commit/a5107dfb566561d9b8d3155f0a8a87096de78ee2)), closes [#7](https://github.com/customerio/customerio-flutter/issues/7) [#8](https://github.com/customerio/customerio-flutter/issues/8)
*   updated android dependency to auto update ([#24](https://github.com/customerio/customerio-flutter/issues/24)) ([040cef2](https://github.com/customerio/customerio-flutter/commit/040cef205821c9bb1913a378dfffa356a3b25650))
*   updated icon and typo ([57c6eef](https://github.com/customerio/customerio-flutter/commit/57c6eefb7d77bc88972450d75e74f1982abb8b16))

### 1.0.0-alpha.9

February 16, 2023 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/1.0.0-alpha.9)

#### Bug Fixes

*   updated android dependency to auto update ([#24](https://github.com/customerio/customerio-flutter/issues/24)) ([040cef2](https://github.com/customerio/customerio-flutter/commit/040cef205821c9bb1913a378dfffa356a3b25650))

### 1.0.0-alpha.8

February 13, 2023 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/1.0.0-alpha.8)

#### Features

*   added missing methods ([#17](https://github.com/customerio/customerio-flutter/issues/17)) ([73f29e6](https://github.com/customerio/customerio-flutter/commit/73f29e64cb58c24f97f3654c5519e22e5255c507))

### 1.0.0-alpha.7

February 9, 2023 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/1.0.0-alpha.7)

#### Bug Fixes

*   in-app remove gist org id ([#19](https://github.com/customerio/customerio-flutter/issues/19)) ([ce4cc9e](https://github.com/customerio/customerio-flutter/commit/ce4cc9e4dd596ba2ff47e02199096154eac8ec06))

### 1.0.0-alpha.6

January 1, 2023 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/1.0.0-alpha.6)

#### Bug Fixes

*   formatting issues ([d67fa7e](https://github.com/customerio/customerio-flutter/commit/d67fa7eea264d20e1b6314277bef926150625f4e))

### 1.0.0-alpha.5

January 1, 2023 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/1.0.0-alpha.5)

#### Bug Fixes

*   plugin version in user-agent ([a10e482](https://github.com/customerio/customerio-flutter/commit/a10e482de83bb4f648408f99a2bed1cade6561c7))

### 1.0.0-alpha.4

December 30, 2022 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/1.0.0-alpha.4)

#### Bug Fixes

*   obj-c bindings issue ([0dbe4ef](https://github.com/customerio/customerio-flutter/commit/0dbe4ef4a893df10d5bf060b6635240699357fa1))

### 1.0.0-alpha.3

December 29, 2022 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/1.0.0-alpha.3)

#### Bug Fixes

*   missing methods and extra dependency ([2c5deca](https://github.com/customerio/customerio-flutter/commit/2c5decac44288e6fba439c1a5669032a0282c69c))

### 1.0.0-alpha.2

December 22, 2022 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/1.0.0-alpha.2)

#### Bug Fixes

*   updated icon and typo ([57c6eef](https://github.com/customerio/customerio-flutter/commit/57c6eefb7d77bc88972450d75e74f1982abb8b16))

### 1.0.0-alpha.1

December 22, 2022 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/1.0.0-alpha.1)

#### Features

*   added SDK config ([#1](https://github.com/customerio/customerio-flutter/issues/1)) ([e8ed7dd](https://github.com/customerio/customerio-flutter/commit/e8ed7ddef985895840a4e406c1a5bb35250c7096))
*   tracking and in-app added ([#2](https://github.com/customerio/customerio-flutter/issues/2)) ([c23f2d9](https://github.com/customerio/customerio-flutter/commit/c23f2d936b801692618c5938ab3d32183345fbbe))

#### Bug Fixes

*   typo fixed ([#9](https://github.com/customerio/customerio-flutter/issues/9)) ([a5107df](https://github.com/customerio/customerio-flutter/commit/a5107dfb566561d9b8d3155f0a8a87096de78ee2)), closes [#7](https://github.com/customerio/customerio-flutter/issues/7) [#8](https://github.com/customerio/customerio-flutter/issues/8)

## 0.x Releases

* * *

### 0.0.1-alpha.3

December 14, 2022 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/0.0.1-alpha.3)

### 0.0.1-alpha.2

December 12, 2022 · [View on GitHub](https://github.com/customerio/customerio-flutter/releases/tag/0.0.1-alpha.2)

---

## Whats new > Ios 27 uiscene support

**Source:** /integrations/sdk/flutter/whats-new/ios-27-uiscene-support

# iOS 27 & UIScene support

The Customer.io Flutter SDK keeps working when your app adopts iOS's UIScene lifecycle or is built with Xcode 27. AppDelegate-only apps need no change.

`customer_io` 4.4.0 and newer keep Customer.io working when your Flutter app adopts iOS’s `UIScene` lifecycle or is built with Xcode 27.

*   If your app keeps the AppDelegate-only lifecycle, **no change is required**.
*   Flutter’s `UIScene` APIs are available from Flutter 3.38 and become the default in 3.41. Customer.io supports scene hosts on Flutter 3.44.8 or newer. The same version contains a separate Xcode 27 build-tool fix. AppDelegate-only apps keep the published `customer_io` minimum.
*   Under `UIScene`, Customer.io push and in-app destinations route through Flutter automatically when the standard `UIApplicationSceneManifest` is present and Flutter deep linking is enabled. Ordinary links stay owned by Flutter; Customer.io unwraps and attributes Live Activity links, then hands the destination to your Dart router.
*   Live Activity scene attribution also needs the opt-in Live Activities module and a scene-delegate-capable registrar (Flutter 3.44.8+).

See the [iOS 27 UIScene migration guide](/integrations/sdk/ios/whats-new/ios-27-uiscene-migration/) for step-by-step Flutter instructions.

---

