GitHub project

Set up live notifications

Updated August 19, 2026

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 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_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.

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.

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'

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.

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.

OptionTypeDefaultDescription
typesList<LiveActivityTemplate>[]Built-in templates to enable. Don’t list custom here—customType enables it.
customTypeString?nullReverse-DNS identifier for your own type, such as com.myapp.rideshare.
brandingLiveActivitiesBranding?nullStyling 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.