GitHub project

Live Activities API and payload reference

Updated August 18, 2026

Your server drives every Live Activity through the Live Notifications API. This page covers the operations, the two objects you send with each one, the bundled templates, and the APNs payload that arrives on the device.

For the app-side work—defining your activity’s data model, building your widget, and initializing the module—see Set up Live Activities.

Notification types

Every live notification belongs to a notification type. Each type has an identifier in reverse-DNS notation, like io.customer.order-tracking, so the identifier stays unique. Identifiers work well in code but are hard to scan in a list, so you can also assign a friendly name that appears in your activity logs.

A type is the kind of thing you track—an order, a ride, a match. Each running notification is an instance of that type, with its own instance_id. Your update, end, and status calls all reference a notification by its instance_id.

Customer.io records a notification type automatically when it receives the type’s SDK registration or its first start request. You can view your types and edit their friendly names under Settings > Live notifications. There’s no create or delete action, and you can’t edit an identifier.

The live notification types table in workspace settings, showing each type's friendly name and reverse-DNS identifier

Activity lifecycle

Every live notification has a unique instance_id that you use to track its lifecycle and target its updates. Each instance holds every delivery made over the notification’s lifespan.

An instance can move through these states:

StateMeaning
activeThe notification is running and can receive updates.
endedYour app or server ended it.
expiredIt passed its expiration without being ended.
failedCustomer.io couldn’t deliver to it.

Start a notification

You can start a live notification two ways:

  • From your app: Your app starts the notification on the device—the moment a customer places an order, for example. This works while your app is in the foreground.
  • From your server: Your server calls the start endpoint and Customer.io pushes to the device. Your app doesn’t need to be open.

Either way, the SDK reports the notification to Customer.io, and Customer.io pushes updates to it from that point on.

Update a running notification

Your server updates live notifications through the update endpoint, referencing the notification by its instance ID. Customer.io looks up the notification’s token and pushes the new content state to the device. The operating system re-renders it without launching your app.

Each update must carry the full content state. You can’t send partial updates that contain only the fields that changed.

Your app can also update a notification locally through the SDK. But SDK-side updates only change the notification on the device. The update doesn’t appear in the Customer.io delivery timeline. (Note that SDK-managed local starts and ends do appear in the delivery timeline.)

End a notification

There are three ways to end a live notification:

  • Your server sends an end request to Customer.io, which delivers a final push that transitions the notification to its ended state. You can include a final content_state to show before it ends; it defaults to the last state you sent.
  • Your app ends it locally, and the SDK reports the end to Customer.io.
  • Automatic expiration. Customer.io marks a notification expired 6 hours after it starts by default. You can set an earlier expiration with the expiration field on the start request, but 6 hours is the maximum. Expiration changes the status in Customer.io—it doesn’t send an end push or clear the notification from the device.

Server control and app control aren’t exclusive: you can start an activity in your app for instant feedback and hand it to your server for updates, or start it from your server and end it in your app. However, local updates never enter the delivery timeline, so a notification you drive from both places will have gaps in its Customer.io history.

Review deliveries

Live notifications are a delivery channel in Customer.io. Every start, update, and end is a separate delivery, so one notification produces a timeline of deliveries over its lifespan. Each delivery moves through various statuses: queued, sent, attempted, failed, or undeliverable. In general, queued and sent deliveries are good states; failed and undeliverable deliveries mean your live notification didn’t make it to the device.

You can see every live notification sent to a person, and drill into each instance’s full delivery timeline, on their profile. See Monitor live notifications for more.

Attributes and content state

When you start, update, or end a live notification through the API, you send two objects: attributes and content_state. Customer.io wraps them in the right platform payload and delivers them to the device.

  • attributes describe the thing the notification is about—the order, the flight, the match. You set them when the notification starts, and they stay fixed for its lifetime.
  • content_state is what the notification currently shows—the status, the score, the ETA. It changes with every update, and the device re-renders from it each time.

When you design a notification type, that’s the test for where a field goes: if it can change while the notification is running, it belongs in content_state. If it identifies what the notification is tracking, it’s an attribute.

A few conventions apply:

  • Dates and timestamps are epoch seconds, not milliseconds. Use the Unix format.
  • Each update carries the full content state. The device re-renders from the complete state on every push. There are no partial updates.
  • Static fields behave differently per platform. On iOS, ActivityKit fixes attributes when the activity is created and keeps them for its lifetime, so you send them only on start. On Android, the device re-renders from each push, so re-send attributes on every update and end—otherwise the static fields disappear.

Attributes are immutable on iOS

This split is ActivityKit’s own model: attributes populates your ActivityAttributes type, and content_state populates its ContentState. ActivityKit fixes an activity’s attributes when the activity is created, so Customer.io sends them only on the start push. Anything that changes over the activity’s life belongs in content_state.

Built-in templates

The built-in templates are the lowest-effort way to ship a live notification: when one of the two shapes—a countdown or a step-by-step tracker—fits what you need, Customer.io renders it for you. You don’t have to write any UI code. You control the content the template shows (and, on Android, template branding like an accent color and logo), but the layout is fixed—you can’t rearrange a built-in template’s views. For anything else, you need to define a custom type and render it yourself.

Both built-in templates use the same notification type identifiers on iOS and Android, so one cross-platform notification can share a single notification_type. You send static fields in attributes and dynamic fields in content_state; each template below shows an example, followed by schemas for both objects. The fields the templates render verbatim (header, title, status, substatus) are freeform text, but the supported fields are only the ones in each schema—the bundled templates don’t expose generic status-color, image, or logo fields.

Countdown timer

Use this template to create a countdown timer, with the notification type io.customer.livenotifications.countdowntimer. A complete start request wraps the template’s attributes and content_state (documented below) in the standard start request fields—identifiers, notification_type, and push_payload:

{
    "identifiers": { "email": "person@example.com" },
    "notification_type": "io.customer.livenotifications.countdowntimer",
    "push_payload": { "alert": { "title": "Summer sale is live", "body": "Tap to see today's deals" } },
    "attributes": {
        "header": "Summer Sale"
    },
    "content_state": {
        "title": "Flash sale ends in",
        "statusMessage": "30% off everything",
        "endTime": 1753603600
    }
}

Attributes

  • headerstringrequired
    Top-row label of the notification.

Content state

  • titlestringrequired
    Primary status line.
  • statusMessagestring
    Secondary line under the title.
  • endTimeinteger
    Countdown target, in whole seconds since 1970 UTC. A time in the future renders a live countdown; omit it to render no timer. The countdown does not clear itself when it reaches zero — it rests at \"0:00\" until you send an update with a finished title and no endTime.

Multi-step tracker

Use this template to create a delivery process tracker, or any tracker with steps, with the notification type io.customer.livenotifications.segments. A complete start request wraps the template’s attributes and content_state (documented below) in the standard start request fields—identifiers, notification_type, and push_payload:

{
    "identifiers": { "email": "person@example.com" },
    "notification_type": "io.customer.livenotifications.segments",
    "push_payload": { "alert": { "title": "Your order is on its way", "body": "Track it live on your Lock Screen" } },
    "attributes": {
        "header": "Chica's Tacos"
    },
    "content_state": {
        "status": "Preparing your order",
        "substatus": "We'll let you know when it's on the way",
        "segmentsTotal": 4,
        "segmentsComplete": 2,
        "trailingText": "25 min"
    }
}

Attributes

  • headerstringrequired
    Top-row label

Content state

  • statusstringrequired
    Primary status line, like Out for delivery.
  • substatusstring
    Secondary line under the status.
  • segmentsTotalintegerrequired
    The total number of segments in the progress bar. Values above 20 are capped at 20.
  • segmentsCompleteintegerrequired
    How many segments are filled; the remainder render as incomplete. Values above segmentsTotal are capped at segmentsTotal.
  • trailingTextstring
    Short text on the Dynamic Island trailing edge, e.g. \"5 min\". Keep it brief; the trailing region is narrow.

What Customer.io sends to APNs

You never build the APNs payload—Customer.io assembles it from the objects you send to the API, and ActivityKit decodes it into your types. But three parts of it are a contract your Swift code has to match:

  • attributes-type is the name of your ActivityAttributes type. It rides only on the start push (attributes are immutable once the activity is created) and must match a registered type, or ActivityKit can’t create the activity. Customer.io injects cioInstanceId into attributes so the SDK can correlate the activity.
  • content-state decodes directly into your ContentState, so its field names have to match your Swift properties. It rides on every push.
  • cioMetadata, injected into content-state, carries the delivery ID, delivery token, and deep link. Declare it on your ContentState (the SDK’s CIOMetadataCarrying protocol) to get tap-through deep links and delivery attribution.

The alert you set with push_payload.alert becomes the notification’s alert: required on an iOS start, optional on updates and ends (include both title and body when you do). This is identical whether your app receives push directly through APNs or nested in Firebase’s FCM envelope—the content doesn’t change.