> This page is part of the [Customer.io documentation](https://docs.customer.io). For the complete index, see [llms.txt](https://docs.customer.io/llms.txt).
> Last updated: September 1, 2026
> To take actions in Customer.io from an agent, use the [Customer.io CLI](https://docs.customer.io/ai/cli/get-started.md) or the [MCP server](https://docs.customer.io/ai/mcp/get-started.md).
> This page has tabbed examples for: Automatic initialization, Manual initialization, Expo Router, Without Expo Router. Add `?tab=<name>` to the URL to get only that tab, and repeat it for more than one, for example `?tab=automatic-initialization&tab=manual-initialization`.

# 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 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 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](/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.

iOS normally requires a fair amount of Xcode work for live notifications. The Expo config plugin does that work for you at prebuild: it adds the widget extension target, sets the `NSSupportsLiveActivities` key, and adds the iOS pod subspec. On the **AppDelegate path** (Expo SDK 53–57), it also injects the `AppDelegate` code that attributes taps. On the **scene lifecycle** (Expo SDK 58+), that AppDelegate tap handling is removed and attribution moves to JavaScript. See [Live Activity links on the scene path](#scene-live-activity-links). Android needs no build-time setup at all.

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/expo/push-notifications/push/) for your app.
*   Use **Expo SDK 53 or later**.
*   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.

## Configure the plugin

How you turn on live notifications depends on how you initialize the SDK.

<!-- tabs -->

<!-- tab: Automatic initialization -->

#### Automatic initialization

Add a `liveNotifications` object under `config` in your `app.json`. Its presence enables the feature—you don’t also need `liveNotifications.enabled`.

```
{
   "expo": {
      "plugins": [
         [
            "customerio-expo-plugin",
            {
               "config": {
                  "cdpApiKey": "<CDP_API_KEY>",
                  "region": "us",
                  "liveNotifications": {
                     "types": [
                        "io.customer.livenotifications.segments",
                        "io.customer.livenotifications.countdowntimer"
                     ]
                  }
               }
            }
         ]
      ]
   }
}
```

<!-- /tab -->

<!-- tab: Manual initialization -->

#### Manual initialization

When you initialize the SDK from JavaScript, there’s no `config` block for the plugin to read, so turn the build-time setup on explicitly with `enabled`:

```
{
   "expo": {
      "plugins": [
         [
            "customerio-expo-plugin",
            {
               "liveNotifications": {
                  "enabled": true
               }
            }
         ]
      ]
   }
}
```

Then pass your notification types to `CustomerIO.initialize` in your app’s code, the same way you would in [React Native](/integrations/sdk/react-native/live-notifications/set-up/#initialize-the-sdk-with-live-notifications).

<!-- /tab -->

<!-- /tabs -->

Run `npx expo prebuild` after you change these options, so the plugin regenerates the native projects. You only need `--clean` when you add or rename `customWidget` or `customRenderer` source files—once the widget target exists, the plugin re-syncs file contents but doesn’t discover new files.

To turn the feature off on either path, set `enabled: false` in the top-level `liveNotifications` block—it wins over everything else.

Omitting `types` enables **every** built-in template, which is what an app that picks its types at runtime needs. To register only your own custom type, set `types` to an empty array alongside `customType`. Unrecognized identifiers are ignored with a warning, so a template added in a newer SDK won’t break your build.

### Where each option goes

The plugin reads two separate `liveNotifications` blocks, and the distinction matters:

Option

Where it goes

Why

`types`, `customType`

Under `config`

Applied when the SDK initializes.

`enabled`, `branding`, `customWidget`, `customRenderer`

Top level, next to `config`

Consumed at build time. Keeping them outside `config` is what makes them work on both initialization paths.

### Branding

Branding applies to the built-in templates. The plugin compiles it into the generated iOS widget and passes it to the Android SDK, so one block covers both platforms:

```
"liveNotifications": {
   "branding": {
      "accentColorHex": "#1B5E20",
      "backgroundColorHex": "#FFFFFF",
      "textColorHex": "#1A1A1A",
      "logo": "./assets/brand-logo.png"
   }
}
```

`accentColorHex` tints the Android notification and fills the iOS progress bar. `backgroundColorHex` and `textColorHex` style the generated iOS widget only. Color values must be six-digit `#RRGGBB`—anything else fails prebuild.

`logo` takes either a project-relative path or an `http(s)` URL. The plugin copies a local path into your Android drawable resources and the iOS widget asset catalog. A URL is downloaded at render time on Android and **isn’t supported on iOS**, where the widget is compiled ahead of time.

Manual initialization needs branding in both places

If you initialize the SDK from JavaScript, set `branding` in the plugin options *and* pass it to `CustomerIO.initialize`. The plugin’s value is what it compiles into the iOS widget; the runtime value is what Android applies. On that path the plugin generates no initialization code, so it can’t forward the value for you.

## Live Activity links on the scene path (Expo SDK 58+)

Expo SDK 58 is pre-release

Stable Expo SDK 57 apps keep the AppDelegate path. Apply this section after Expo SDK 58 is available as a stable release.

If you build with **Expo SDK 58 or newer** (using `customerio-expo-plugin` 3.9.0 and `customerio-reactnative` 6.9.0), your iOS project uses the scene lifecycle. On this path the plugin no longer injects the AppDelegate Live Activity URL handling it used to add, so **Live Activity attribution moves to JavaScript**. Add this step in the same upgrade. Without it, taps on a Live Activity render but don’t report an `opened` metric or route their destination.

Process each incoming URL through `CustomerIO.liveActivities.handleWidgetUrl(path)` **exactly once**. The helper reports the opened event, then returns the URL your app should route. An ordinary URL passes through unchanged, and a Customer.io tracking URL with no destination returns `null`.

<!-- tabs -->

<!-- tab: Expo Router -->

#### Expo Router

Process Live Activity URLs once in a top-level `app/+native-intent.tsx`:

```
import { CustomerIO } from 'customerio-reactnative';

export async function redirectSystemPath({ path }: { path: string }) {
  return CustomerIO.liveActivities.handleWidgetUrl(path);
}
```

If the helper returns `null`, Expo Router performs no redirect and keeps the current path.

Expo Router owns navigation after `redirectSystemPath` returns the destination.

<!-- /tab -->

<!-- tab: Without Expo Router -->

#### Without Expo Router

Apply the same helper exactly once in your central initial-URL and URL-subscription pipeline. This differs from Customer.io notification destinations: a cold Live Activity tap is the system launch URL, while a cold notification destination is buffered by the Customer.io bridge and emitted as a `url` event after you signal readiness.

```
import { Linking } from 'react-native';
import { CustomerIO } from 'customerio-reactnative';

async function routeUrl(incomingUrl: string) {
  const destination = await CustomerIO.liveActivities.handleWidgetUrl(incomingUrl);
  if (destination) {
    // navigate to destination with your router
  }
}

// initial URL (cold start)
Linking.getInitialURL().then((url) => {
  if (url) routeUrl(url);
});

// subsequent URLs (warm)
Linking.addEventListener('url', ({ url }) => routeUrl(url));
```

<!-- /tab -->

<!-- /tabs -->

Warning

Don’t call `handleWidgetUrl` from both `+native-intent.tsx` and a `Linking` listener. Processing the same tracking URL twice reports two opened events.

Below Expo SDK 58, the plugin keeps injecting the AppDelegate tap handling and no JavaScript change is required. For the full cross-SDK picture, see the [iOS 27 UIScene migration guide](/integrations/sdk/ios/whats-new/ios-27-uiscene-migration/).

## 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 hand the plugin the source files that render it. You don’t need a widget target of your own—the plugin compiles your SwiftUI into the one it generates.

```
{
   "config": {
      "liveNotifications": {
         "types": ["io.customer.livenotifications.segments"],
         "customType": "com.myapp.rideshare"
      }
   },
   "liveNotifications": {
      "customWidget": {
         "sourceFile": "./ios-widgets/RideshareLiveActivity.swift",
         "structName": "RideshareLiveActivity"
      },
      "customRenderer": {
         "sourceFile": "./android-renderers/RideshareLiveNotification.kt",
         "className": "RideshareLiveNotificationCallback"
      }
   }
}
```

*   `customWidget.structName` is the `Widget` struct the plugin instantiates in the generated `WidgetBundle`. Render the SDK’s `CIOCustomAttributes` in it, and apply `.cioWidgetUrl(context.state.cioMetadata)` to your Lock Screen view and Dynamic Island regions—the plugin doesn’t wire tap attribution into your SwiftUI, so without it taps on your custom notification aren’t attributed and don’t route their deep link.
*   `customRenderer.className` is a `CustomerIOLiveNotificationsCallback` class with a no-argument constructor. It receives the same `data` map your app sends.

Configure both. Setting only one leaves the notification without a renderer on the other platform. Each option also accepts an array of paths when your renderer spans several files.

Then start it with the `Custom` template:

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

[GitHub project](https://github.com/customerio/customerio-expo-plugin "View the Expo SDK on GitHub")

Version

3.11.1 (Current)1.x (1.0.0-beta.17, prerelease)

©2026 Peaberry Software, Inc. [Status](https://status.customerio.com/) [Terms of Service](https://customer.io/legal/terms-of-service/) [Privacy Policy](https://customer.io/legal/privacy-policy/)

[](https://www.linkedin.com/company/customer-io)[](https://twitter.com/customerio)[](https://www.youtube.com/channel/UCkCaWdezRoa8ZyR9pEVaipA)[](https://www.instagram.com/customer.io/)
