GitHub project

Set up Live Activities

Updated August 18, 2026

Live Activities push real-time, continuously updated notifications to the iOS Lock Screen and Dynamic Island—a delivery tracker, a live score, 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.

Minimum requirements

Live Activities require iOS 16.2 or later—iOS 17.2 or later for push-to-start (starting activities from your server). Your app must also have the push module (MessagingPushAPN or MessagingPushFCM) configured and must identify the profile before the SDK registers tokens: Live Activities register their tokens and report every lifecycle event through the device’s push token, so without one, nothing registers or reports. Live Activities are available on the native iOS SDK installed with either Swift Package Manager or CocoaPods.

How it works

A Live Activity is Apple’s format for glanceable, continuously updated content on the Lock Screen and in the Dynamic Island, built on the ActivityKit framework. Unlike a standard push, it doesn’t arrive once and sit in a list—it’s a persistent surface that re-renders each time new state arrives. Apple’s Human Interface Guidelines cover what makes a good one.

Four actors drive every Live Activity:

  • You define an ActivityKit ActivityAttributes type 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.
  • Customer.io turns each API call into an APNs push, using the tokens the SDK registered, and records every start, update, and end as a delivery.
sequenceDiagram participant Widget as Your widget participant SDK as Customer.io SDK participant CIO as Customer.io participant Server as Your server SDK->>CIO: Register push tokens Server->>CIO: Start the activity (API call) CIO->>Widget: Push to start (APNs) Note over Widget,SDK: iOS receives each push, creates the<br>activity, and re-renders your widget—<br>the app and SDK aren't in the render path SDK->>CIO: Report lifecycle events Server->>CIO: Update, then end (API calls) CIO->>Widget: Push each new content state (APNs)

Customer.io sits in the middle: your server only ever calls the API, the SDK keeps Customer.io supplied with the device’s push tokens, and APNs delivers each update to the widget you built. The SDK never handles the pushes themselves—iOS applies them directly, which is why updates keep rendering when your app isn’t running. For the API operations, payload shapes, and bundled templates, see the API and payload reference.

Before you begin

Complete two one-time setup steps in Xcode:

  1. Add a Widget Extension target (File > New > Target > Widget Extension) if you don’t already have one. Your Live Activity UI lives here.
  2. Enable Live Activities by setting NSSupportsLiveActivities to YES in your app target’s Info settings. If your activity updates frequently, also set NSSupportsLiveActivitiesFrequentUpdates to YES.

Live Activities use your existing push credentials, APNs or FCM. The SDK calls registerForRemoteNotifications without requesting alert authorization. This registers the APNs token—and, in the Firebase integration, obtains and registers the FCM token—without showing a notification-permission prompt.

You must also identify your app user before you can start an activity. The SDK holds token registrations until you identify a person, and the API rejects requests that target anonymous profiles.

Live Activities don't require notification permission

ActivityKit generates its own tokens independently of notification permission, so your customers can receive live notifications even if they’ve denied push notifications—on either delivery path—and you don’t have to manage a permission prompt.

Step 1: Add the SDK packages

Live Activities require two packages. Add each to the correct target, using the names for your dependency manager:

InstallationApp targetWidget extensionTemplates (optional)
Swift Package ManagerLiveActivitiesLiveActivities_AttributesLiveActivities_Templates
CocoaPodsCustomerIOLiveActivitiesCustomerIOLiveActivitiesAttributesCustomerIOLiveActivitiesTemplates

The attributes package (LiveActivities_Attributes or CustomerIOLiveActivitiesAttributes) has no SDK dependencies. It ships the types your widget shares with your app, the epoch-second date decoder, and the .cioWidgetUrl widget modifier—but none of the SDK’s networking or lifecycle machinery. That’s why it’s safe to import in your widget extension, which needs it to share your activity’s data model with your app. You’ll add it to both the app target and the widget extension target.

If you want to use our pre-configured templates, you’ll also add the templates library to both targets—LiveActivities_Templates with Swift Package Manager, or CustomerIOLiveActivitiesTemplates with CocoaPods.

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. A single source file for both targets 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?
    }
}
  • CIOActivityAttribute is a one-property protocol (cioInstanceId) that opts your activity type into push-to-start, so your server can create the activity remotely. A plain ActivityAttributes type also works. The SDK still observes it for local starts, updates, and ends, but you can only start conforming activity types from your server.
  • CIOMetadataCarrying is optional. Declare cioMetadata on your ContentState to receive each update’s deep link and delivery attribution.
  • EpochSecondsDate decodes 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.

Use .cioWidgetUrl(context.state.cioMetadata) so a tap opens the deep link your server attached to the latest push and is attributed to the exact delivery. Make sure you add the widget to your extension’s WidgetBundle.

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)
                .cioWidgetUrl(context.state.cioMetadata)
        } 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")
            }
        }
    }
}

Step 4: Initialize the module

Add the Live Activities module to your SDK configuration and register each activity type you want Customer.io to observe. The module is registered with the main SDK, so there’s no separate instance to hold onto—you drive activities through CustomerIO.liveActivities.

import CioDataPipelines
import CioLiveActivities

let configBuilder = SDKConfigBuilder(cdpApiKey: "YOUR_CDP_API_KEY")

if #available(iOS 16.2, *) {
    configBuilder.addModule(
        LiveActivitiesModule(
            config: LiveActivityConfigBuilder()
                .register(
                    DeliveryActivityAttributes.self,
                    identifier: DeliveryActivityAttributes.identifier
                )
                .build()
        )
    )
}

CustomerIO.initialize(withConfig: configBuilder.build())

The register call tells the SDK which activity type to watch. From here on, the SDK observes any activity of that type automatically, registers its tokens with Customer.io, and reports its lifecycle. There’s no per-activity code.

When a customer taps an activity, and the latest push carried a deep link, iOS opens your app with the widget URL. Pass it through CustomerIO.liveActivities.handleWidgetUrl(_:) before your own routing so the tap is attributed to the exact delivery:

func application(_ app: UIApplication, open url: URL,
                 options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
    guard let destination = CustomerIO.liveActivities.handleWidgetUrl(url) else {
        return true
    }
    return handleDeepLink(destination)
}

handleWidgetUrl records the tap for metrics. For non-Customer.io URLs, it returns the original URL unchanged. For a Customer.io widget URL, it returns the embedded destination, or nil if no destination was provided.

This means that if you intend for a notification to deep link into your app, you should include it in every payload you send.

That’s the setup. To create your first activity—from your server with push-to-start, or from your app—see Start Live Activities.