Live Activities
PremiumThis feature is available for Premium plans. EnterpriseThis feature is available for Enterprise plans. UpdatedLive Activities let you push real-time, continuously updated notifications to the iOS Lock Screen and Dynamic Island, like a delivery tracker, a live score, or a countdown. Define your activity’s data model, render it with your own widget, and let Customer.io push updates without your app being open.
Rollout in progress
See How live notifications work to learn more about the live notification lifecycle.
Requirements
How the pieces fit
- You define an ActivityKit
ActivityAttributestype describing your activity’s data, and a widget that renders it. The SDK never renders your activity—the design is entirely yours. - The SDK observes activities of the types you register, captures and registers push tokens, and reports lifecycle events to Customer.io.
- Your server starts, updates, and ends activities by calling the API—it never talks to APNs.
Before you begin
You need to complete two one-time setup steps in Xcode:
- Add a Widget Extension target (File > New > Target > Widget Extension) if you don’t already have one. Your Live Activity UI lives here.
- Enable Live Activities by setting
NSSupportsLiveActivitiestoYESin your app target’s Info settings. If your activity updates frequently, also setNSSupportsLiveActivitiesFrequentUpdatestoYES.
Step 1: Add the SDK packages
Live Activities span two libraries. You need to add each to the correct target in your Swift Package Manager settings:
| Library | Add to |
|---|---|
LiveActivities | App target |
LiveActivities_Attributes | App target and widget extension target |
LiveActivities_Attributes contains only protocols and value types—no SDK machinery—so it’s safe to import in your widget extension, which needs it to share your activity’s data model with the app.
Step 2: Define your activity’s data model
Create an ActivityAttributes type describing your activity. Make the file a member of both your app target and your widget extension target—ActivityKit matches the two sides by the type’s name and Codable shape, so sharing one source file is the intended pattern.
import ActivityKit
import CioLiveActivities_Attributes
struct DeliveryActivityAttributes: CIOActivityAttribute {
// Reverse-DNS identifier, registered with the SDK and matched
// server-side to route pushes.
static let identifier = "io.customer.liveactivities.deliverytracking"
// Managed by Customer.io. Declare it with a default and never set it
// yourself—the SDK fills it in on local start, and Customer.io fills
// it in for push-to-start.
var cioInstanceId: String = ""
// Your static fields—set once at start, never change.
var orderNumber: String
// Your dynamic fields—updated over the activity's life.
struct ContentState: Codable, Hashable, CIOMetadataCarrying {
var title: String
var subtitle: String?
var estimatedArrival: EpochSecondsDate?
// Deep-link and delivery metadata for the push that produced
// this state. nil for locally-driven updates.
var cioMetadata: CIOLiveActivityMetadata?
}
}
Three Customer.io-specific pieces to note:
CIOActivityAttributeis a one-property protocol (cioInstanceId) that opts your activity type into push-to-start, so your server can create the activity remotely. A plainActivityAttributestype also works. The SDK still observes it for local starts, updates, and ends, but you can only start conforming activity types from your server.CIOMetadataCarryingis optional. DeclarecioMetadataon yourContentStateto receive each update’s deep link and delivery attribution.EpochSecondsDatedecodes the epoch-second date fields Customer.io sends, so live countdowns work without custom decoding.
Step 3: Build your widget
Render your activity in your widget extension with a standard ActivityKit ActivityConfiguration. This is your SwiftUI—Customer.io doesn’t constrain the design.
import ActivityKit
import CioLiveActivities_Attributes
import SwiftUI
import WidgetKit
struct DeliveryActivityWidget: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: DeliveryActivityAttributes.self) { context in
// Your Lock Screen view
DeliveryLockScreenView(state: context.state)
.widgetURL(context.state.cioMetadata?.deepLink.flatMap(URL.init(string:)))
} dynamicIsland: { context in
DynamicIsland {
DynamicIslandExpandedRegion(.bottom) {
Text(context.state.title)
}
} compactLeading: {
Image(systemName: "shippingbox.fill")
} compactTrailing: {
Text(context.state.subtitle ?? "")
} minimal: {
Image(systemName: "shippingbox.fill")
}
}
}
}
Set .widgetURL from cioMetadata?.deepLink to make taps open the deep link your server attached to the latest push. Add the widget to your extension’s WidgetBundle.
Step 4: Initialize the module
After you initialize the Customer.io SDK, initialize the Live Activities module and register each activity type you want Customer.io to observe. It isn’t a singleton, so keep a reference to the returned instance for the lifetime of your app.
import CioDataPipelines
import CioLiveActivities
let config = SDKConfigBuilder(cdpApiKey: "YOUR_CDP_API_KEY").build()
CustomerIO.initialize(withConfig: config)
if #available(iOS 17.2, *) {
self.liveActivities = LiveActivitiesModule.initialize(
LiveActivityConfigBuilder()
.register(
DeliveryActivityAttributes.self,
identifier: DeliveryActivityAttributes.identifier
)
.build()
)
}
The register call tells the SDK which activity type to watch. From here on, any activity of that type is observed automatically. The SDK registers its tokens with Customer.io and reports its lifecycle. There’s no per-activity code.
Step 5: Start an activity
There are two ways to start a live activity:
- From your server (push-to-start): You call the API when you’re ready to start an activity and iOS creates it on the device. Your app doesn’t need to be open. This is the most common path for activities like delivery tracking and live scores, and requires iOS 17.2.
- From your app: Start the activity through the module when something happens in the app. This is a common path for activities like a workout progress tracker.
From your server
Call the start endpoint to start an activity for a person. Customer.io sends the push-to-start message, and iOS creates the activity with the attributes and content state from your payload.
From your app
Start the activity through the module—when a customer places an order, for example:
let activity = try liveActivities.start(
DeliveryActivityAttributes(orderNumber: "CIO-1234"),
contentState: .init(title: "Order received")
)
start returns a CIOLiveActivity handle. You can drive the activity locally through it—activity.update(_:) and activity.end(_:)—and the SDK reports each change to Customer.io so your server stays in sync. If your app already starts activities with raw ActivityKit calls, pass them to liveActivities.adopt(_:) instead so the SDK can track them.
Once the activity is running, your server can push subsequent updates by calling the update endpoint with the activity’s instance ID. You don’t need to manage push tokens yourself.
Handle deep links
When a customer taps an activity, and the latest push carried a deep link, iOS opens your app with that URL. You need to pass it to the module to open the app:
func application(_ app: UIApplication, open url: URL,
options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
if liveActivities?.handleDeepLinkOpen(url) == true {
return true
}
// ... your other URL handling
return false
}
handleDeepLinkOpen records the tap for metrics and returns true when the URL came from a live notification.
