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—so 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. 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.
Prerequisites
Live notifications use the push credentials your app already has in Customer.io. Before you begin:
- Set up push notifications for your app.
- You must identify
The Customer.io operation that adds or updates a person. When you identify a person, Customer.io either adds a person if they don't exist in your workspace, or updates them if they do. Before you identify someone (by their email address or an ID), you can track them anonymously.
your app user before you can start a notification. The SDK holds token registrations until you identify a person, and the API 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_NOTIFICATIONSon Android 13 and later. To get Android 16’s promoted Live Updates treatment, also declarePOST_PROMOTED_NOTIFICATIONSin 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.
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.
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=trueIf 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 iosThen 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.
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
CIOCustomAttributesin your widget extension with your own SwiftUI. - Android: implement
createLiveNotificationin aCustomerIOLiveNotificationsCallbackand register it withCustomerIOLiveActivities.setLiveNotificationCallback. Register it inApplication.onCreate, beforeCustomerIO.initializeruns—the native SDK only accepts the callback at build time, and a push can start your process with no Activity. Your callback receives the samedatamap as the one you pass tostart.