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 React Native 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 JavaScript 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 initialization config.

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

Add the liveactivities subspec to your ios/Podfile. CocoaPods takes one line per pod, so replace the customerio-reactnative line you added during push setup with a single line listing every subspec you use—your push provider (apn or fcm) included:

pod "customerio-reactnative", :path => "../node_modules/customerio-reactnative",
    :subspecs => ["apn", "liveactivities"]

The :path is required. Without it CocoaPods looks for the pod in its trunk repo instead of the copy in your node_modules, and pod install fails.

Your app target needs use_frameworks!

A Live Activity widget links Swift pods, and CocoaPods requires an app target and the extension it embeds to agree on use_frameworks!. If your app target doesn’t have it, the build fails with could not build module 'customerio_reactnative'. Set it once, above your targets, and use static linkage—React Native’s New Architecture doesn’t support dynamic frameworks:

use_frameworks! :linkage => :static

This applies on both push providers, not just Firebase.

Then set NSSupportsLiveActivities in your app target’s 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 wrapper pod, so name the rendering pods in its own Podfile target. Keep the version in step with the native iOS SDK version the wrapper pins, which you’ll find as cioNativeiOSSdkVersion in the SDK’s package.json:

target "LiveActivityWidget" do
  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 customerio_reactnative
import React

extension AppDelegate {
  func application(_ app: UIApplication, open url: URL,
                   options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
    guard let routableUrl = NativeLiveActivities.handleWidgetUrl(url) else { return true }
    return RCTLinkingManager.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.

A React Native AppDelegate conforms to UIApplicationDelegate directly rather than inheriting from a parent class, so this method isn’t an override, and you pass the URL to RCTLinkingManager rather than super. Android needs no equivalent step.

Initialize the SDK with live notifications

Add a liveNotifications object to your initialization config, listing the built-in types your app uses:

import { CustomerIO, CioRegion, LiveActivityTemplate } from 'customerio-reactnative';

CustomerIO.initialize({
  cdpApiKey: 'your-cdp-api-key',
  region: CioRegion.US,
  liveNotifications: {
    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.

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.

liveNotifications: {
  types: [LiveActivityTemplate.Segments],
  branding: {
    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. Don’t add Custom to typescustomType is what enables it:

liveNotifications: {
  types: [LiveActivityTemplate.Segments],
  customType: 'com.myapp.rideshare',
},

Then start it with the Custom template:

await CustomerIO.liveActivities.start({
  type: LiveActivityTemplate.Custom,
  data: { driver: 'Sam', vehicle: 'Blue Prius', minutesAway: '4' },
});

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 NativeLiveActivitiesModule.setLiveNotificationCallback. Register it in Application.onCreate, before the SDK initializes—the native SDK only accepts the callback at build time. Your callback receives the same data map as the one you pass to start.