> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Manage iOS Live Activities end to end

> Start, track, update, and end iOS ActivityKit Live Activities through Orbit — the activity-token handoff, the lifecycle API, push-driven updates, and end semantics, with a full order-tracking example.

# Manage iOS Live Activities end to end

This guide walks the full Live Activity lifecycle: hand the activity token from your iOS app to Orbit, track every running activity, push content updates, and end the activity when the tracked task completes. Follow it once and Live Activities become a closed loop — token in, updates flowing, activity ended cleanly.

For request and response schemas, see the [push API reference](/api-reference/endpoints/push). This page covers the workflow; the [push channel page](/channels/push) covers APNs credential setup. The base push lifecycle — device tokens, targeting, and engagement — lives in the [push integration guide](/guides/push-integration).

## 1. What Live Activities are

A Live Activity (iOS 16.1+, ActivityKit) is a persistent, glanceable surface on the lock screen and in the Dynamic Island. Where a normal push notification delivers a one-shot message and then decays into Notification Center, a Live Activity stays visible and its content updates in place as new pushes arrive — an order moving from Preparing to Out for delivery, a live score, a courier's ETA.

That difference drives the whole lifecycle:

|              | Normal push                                              | Live Activity                                            |
| ------------ | -------------------------------------------------------- | -------------------------------------------------------- |
| Presentation | Single render on delivery                                | Persistent lock-screen / Dynamic Island surface          |
| Updates      | Each send is a new notification                          | Pushes update the same activity in place                 |
| Addressing   | A device token per target                                | One `activity_token` per running activity                |
| End state    | No concept — a notification either renders or it doesn't | Explicit `end` push; iOS dismisses at a `dismissal_date` |

Orbit tracks each activity you start so you can list, update, and end it later by its Orbit id — no need to retain the raw activity token yourself.

## 2. Prerequisites

* **APNs credentials are set as one atomic group** in your tenant's push channel configuration — Live Activity pushes ride the same APNs transport as regular notifications. Follow the [Apple push setup](/channels/push#apple-push-setup-apns) section of the channel page; a partial credential group (key id without team id, private key missing) fails every APNs send.
* **The iOS app starts the activity with ActivityKit** and obtains its APNs token for server-driven updates — `ActivityKit.Activity<T>.pushToken` from your app code. A device token (from `POST /api/v1/push/device-tokens`) is optional but recommended: it ties the activity to a device Orbit already knows.
* **Two stable identifiers decided up front**: an `activity_type` string per surface (`order-status`, `match-score`, `delivery-eta`, ...) and whether you also pass `contact_id` to attribute the activity to a contact for later reconciliation.

## 3. Start a Live Activity

The iOS app starts the ActivityKit activity locally, then your backend registers it with Orbit in one call. `POST /api/v1/push/live-activities` stores the activity token and fires the first `start` push, which tells iOS to bring the surface live:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/push/live-activities \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "activity_token": "<token vended by Activity.pushToken>",
    "activity_type": "order-status",
    "device_token_id": "deviceToken_9f2c",
    "contact_id": "contact_4a1e",
    "content_state": { "status": "Preparing", "etaMinutes": 12 },
    "title": "Order #12345",
    "body": "Preparing — about 12 minutes",
    "apns_priority": 10,
    "relevance_score": 0.8
  }'
```

* `activity_token` (required) — the APNs token from `Activity.pushToken`. Empty or missing values fail schema validation at 422.
* `activity_type` (required) — 1–64 chars. Pick one per surface and keep it stable; it is the fastest way to group listings later.
* `content_state` (required) — a JSON object interpreted by your app's `ActivityContent` shape. Serialize the same keys your ActivityKit attributes decode.
* `device_token_id` / `contact_id` (optional) — link the activity to a known device or contact.
* `title`, `body`, `sound` (optional) — alert content carried on the ActivityKit push; `body` accepts up to 4,000 chars.
* `apns_priority` (optional) — one of `1`, `5`, `10`. Use `10` for time-sensitive updates, lower values when batching.
* `relevance_score` (optional) — 0–1. Higher scores promote the activity on the lock screen.

`POST` returns `201` with the tracked row handle:

```json theme={null}
{
  "data": {
    "id": "liveActivity_8k2mqa",
    "activity_type": "order-status",
    "status": "sent"
  },
  "meta": { "request_id": "req_...", "timestamp": "2026-08-31T12:00:00Z" }
}
```

`status` is the APNs send result — `sent` when the provider accepted the `start` push, `failed` with an `error` string when it did not. Hold the `id`; every later update and end operation addresses the activity by it.

## 4. Track active Live Activities

`GET /api/v1/push/live-activities` lists every activity this tenant has started, newest first, capped at 200 rows per page. Each row carries the full lifecycle:

```json theme={null}
{
  "data": [
    {
      "id": "liveActivity_8k2mqa",
      "device_token_id": "deviceToken_9f2c",
      "contact_id": "contact_4a1e",
      "activity_token": "…",
      "activity_type": "order-status",
      "content_state": { "status": "Out for delivery", "etaMinutes": 4 },
      "status": "updated",
      "dismissal_date": null,
      "started_at": "2026-08-31T11:48:00Z",
      "updated_at": "2026-08-31T11:56:00Z",
      "ended_at": null
    }
  ],
  "meta": { "request_id": "req_...", "timestamp": "..." }
}
```

* `status` moves through `started` → `updated` → `ended` as the lifecycle advances. A row still `started` never received an update push; a row `ended` is final — filter it out of any "active activities" render.
* `activity_type` is your filter axis: scan the page for the types you care about (`order-status`, `match-score`). The endpoint reads one tenant's rows most-recent-first, so the typical filter-and-paginate loop is client-side over pages until your predicate stops matching.
* `ended_at` / `dismissal_date` tell you when the surface went away (or when iOS will dismiss it); rows with neither still count as live.

The dashboard renders the same list under **Messages → Push → Live Activities**, so operations and engineering read one source of truth.

## 5. Update a Live Activity

`PUT /api/v1/push/live-activities/{id}` pushes a new `content_state` to a running activity — iOS re-renders the surface in place:

```bash theme={null}
curl -X PUT https://api.orbit.devotel.io/api/v1/push/live-activities/liveActivity_8k2mqa \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content_state": { "status": "Out for delivery", "etaMinutes": 4 },
    "title": "Order #12345",
    "body": "Out for delivery — 4 minutes away",
    "apns_priority": 10
  }'
```

* The path `id` is the Orbit id returned at start (`liveActivity_…`), never the raw activity token; a malformed id fails with `422 VALIDATION_ERROR`.
* `content_state` is required and replaces the previous state in full — send the complete object, not a diff. `title`, `body`, `apns_priority`, `relevance_score` are optional per update.
* Returns `200` with `{ "id", "status" }` — the APNs send result. Returns `404 NOT_FOUND` when the id is unknown to this tenant **or the activity has already ended**; treat a 404 as "stop updating this activity" in your loop.

Update pacing is on you: frequent, small updates burn APNs throughput and battery, so advance the state only when it materially changed, and drop `apns_priority` to `5` when batching non-urgent transitions.

## 6. End a Live Activity

`DELETE /api/v1/push/live-activities/{id}` sends the final ActivityKit `end` push and closes the row:

```bash theme={null}
curl -X DELETE https://api.orbit.devotel.io/api/v1/push/live-activities/liveActivity_8k2mqa \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content_state": { "status": "Delivered" },
    "dismissal_date": 1771264800
  }'
```

* The body is optional. `content_state` overrides the last stored state for the final render; omit it and the stored state is reused. `dismissal_date` is Unix-epoch seconds telling iOS when to remove the activity from the lock screen; omit it and the dismissal time defaults to the moment of the call.
* Returns `200` with `{ "id", "status" }` — `sent` when the `end` push was accepted, `failed` with an `error` string when APNs rejected it (the row is still marked `ended`, so the bookkeeping closes either way).
* Returns `404 NOT_FOUND` when the id is unknown to this tenant. Calling DELETE on an already-ended row is safe and idempotent — the row is re-stamped `ended` without re-sending the end push.

End every activity you start. Orphaned activities linger on the user's lock screen until iOS expires them, which reads as a bug in your app.

## 7. End-to-end example: order-status loop

A delivery backend that tracks an order from placement to handoff. The iOS app starts an ActivityKit activity on order confirmation and hands the token to your backend; the backend then owns the lifecycle:

```ts theme={null}
import { Orbit } from '@devotel/sdk';

const orbit = new Orbit({ apiKey: process.env.ORBIT_API_KEY! });

// 1. iOS hands over the token after ActivityKit starts the activity.
const start = await fetch('/api/orders/order-42/live-activity-token', {
  /* …your app endpoint that received the token from the device… */
});
const activityToken: string = /* token from device */;

// 2. Start — returns the Orbit id you will address from here on.
const started = await orbit.request('POST', '/api/v1/push/live-activities', {
  activity_token: activityToken,
  activity_type: 'order-status',
  device_token_id: deviceTokenId,
  content_state: { status: 'Preparing', etaMinutes: 12 },
  title: 'Order #42',
  body: 'Preparing — about 12 minutes',
  apns_priority: 10,
});
const activityId = started.data.id;

// 3. Update as the order advances. Poll/list with
//    GET /api/v1/push/live-activities to reconcile pending rows.
await orbit.request('PUT', `/api/v1/push/live-activities/${activityId}`, {
  content_state: { status: 'Out for delivery', etaMinutes: 4 },
  title: 'Order #42',
  body: 'Out for delivery — 4 minutes away',
});

// 4. End when the courier reports handoff.
await orbit.request('DELETE', `/api/v1/push/live-activities/${activityId}`, {
  content_state: { status: 'Delivered' },
});
```

Every call carries `X-API-Key`; the chained `Idempotency-Key` header dedupes retries on the start call if a timeout forces a re-POST. For error handling, the update and end calls return `404` when the activity is gone — the right signal to drop it from your tracking.

## 8. Deferred updates with scheduled pushes

When an update should land later — "re-check ETA at pick-up time", "offer a wait-time recalculation" — queue it with the scheduled-push surface instead of a sleep in your backend. `POST /api/v1/push/send` with `send_at` holds the payload for the future instant; the queued row is managed (listed, inspected, cancelled) from **Messages → Push → Scheduled pushes** in the dashboard or via `GET / DELETE /api/v1/push/scheduled`. The same gates (suppression, frequency caps) replay at send time.

For the general push lifecycle this guide builds on, start with [Push notifications end to end](/guides/push-integration).
