Start live notifications
Your server can drive a notification’s whole lifecycle through the API, 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:
import { CustomerIO, LiveActivityTemplate } from 'customerio-reactnative';
const activityId = await CustomerIO.liveActivities.start({
type: LiveActivityTemplate.Segments,
header: "Chica's Tacos",
status: 'Order received',
segmentsTotal: 4,
segmentsComplete: 1,
});
await CustomerIO.liveActivities.update(activityId, {
type: LiveActivityTemplate.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 for how each platform handles it.
Taps on the built-in templates are attributed automatically—the plugin injects the iOS AppDelegate forwarding that React Native apps have to add by hand.
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.