diff --git a/src/data/nav/pubsub.ts b/src/data/nav/pubsub.ts
index dee84c0db1..d72e1fa92a 100644
--- a/src/data/nav/pubsub.ts
+++ b/src/data/nav/pubsub.ts
@@ -296,6 +296,10 @@ export default {
name: 'Publish',
link: '/docs/push/publish',
},
+ {
+ name: 'Live Activities',
+ link: '/docs/push/live-activities',
+ },
],
},
{
diff --git a/src/pages/docs/push/live-activities.mdx b/src/pages/docs/push/live-activities.mdx
new file mode 100644
index 0000000000..1696d6727a
--- /dev/null
+++ b/src/pages/docs/push/live-activities.mdx
@@ -0,0 +1,244 @@
+---
+title: iOS Live Activities
+meta_description: "Start, update, and end iOS Live Activities with Ably using APNs broadcast channels, without managing per-activity push tokens."
+meta_keywords: "Live Activity, Live Activities, ActivityKit, push-to-start, APNs broadcast channel, Dynamic Island, iOS push notifications"
+---
+
+Live Activities display an app's live state on the iOS lock screen and in the Dynamic Island, for example a sports scoreboard, a delivery tracker, or the progress of a long-running task. Apple updates Live Activities through APNs rather than through the app itself, which normally means collecting and refreshing a push token for every activity on every device.
+
+Ably removes that bookkeeping. Ably holds your APNs credentials and manages [APNs broadcast channels](https://developer.apple.com/documentation/activitykit/starting-and-updating-live-activities-with-activitykit-push-notifications) on your behalf. Your server creates a broadcast channel once, then starts, updates, and ends Live Activities with three lifecycle calls; each state change is a single publish that APNs fans out to every activity subscribed to the broadcast channel. Devices register themselves using the same [push activation](/docs/push/configure/device?lang=swift#activate) flow used for ordinary push notifications.
+
+
+
+## How it works
+
+A Live Activity integration has three moving parts:
+
+1. Create a broadcast channel: your server asks Ably to create an APNs broadcast channel. Ably returns a pair of identifiers: an opaque broadcast `id`, which your server uses in every subsequent start, update, and end call, and an `apnsChannelId`, which iOS devices use to subscribe an activity to the channel.
+
+2. Start the Live Activity, either:
+ * From the server with push-to-start: devices that have registered a Live Activity push-to-start token with Ably can have activities started remotely. The start payload includes the `apnsChannelId` (as `input-push-channel`), so the started activity is subscribed to the broadcast channel immediately.
+ * Locally in the app: the app starts the activity itself with `pushType: .channel(apnsChannelId)`, subscribing it to the broadcast channel.
+
+3. Update and end by broadcast: each state change is a single publish against the broadcast `id`. APNs fans it out to every subscribed activity on every device, with no per-activity tokens involved. Creating the channel with `messageStoragePolicy: 1` caches the most recent update so late-joining activities receive the current state as soon as they subscribe.
+
+
+
+## Prerequisites
+
+* An Ably app with [APNs configured](/docs/push/configure/device?lang=swift#apns): upload your APNs .p8 authentication key so Ably can push on your behalf.
+* An API key or token with the [`push-admin` capability](/docs/auth/capabilities) for the server-side calls on this page.
+* An iOS app with the `NSSupportsLiveActivities` Info.plist key, a Live Activity widget extension, and an `ActivityAttributes` model.
+* Devices running iOS 18 or later. Live Activities themselves are available from iOS 16.1 and push-to-start from iOS 17.2, but the broadcast channels that Ably uses to deliver updates require iOS 18.
+* Clients should authenticate with [token authentication](/docs/auth/token), for example an `authUrl` pointing at your server, so the API key never ships in the app.
+
+## Register the device
+
+Activate the device with Ably using the standard [push activation](/docs/push/configure/device?lang=swift#activate) flow, then add the Live Activity push-to-start token to the registration. iOS delivers push-to-start tokens through ActivityKit's `pushToStartTokenUpdates` stream:
+
+
+```swift
+let options = ARTClientOptions()
+options.authUrl = URL(string: "https://example.com/api/auth")
+options.pushRegistererDelegate = self
+let rest = ARTRest(options: options)
+
+// Standard activation: registers the device and its APNs device token.
+rest.push.activate()
+```
+
+
+Forward the APNs device token from your `UIApplicationDelegate` as usual:
+
+
+```swift
+func application(_ application: UIApplication,
+ didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
+ ARTPush.didRegisterForRemoteNotifications(withDeviceToken: deviceToken, rest: rest)
+}
+```
+
+
+Once activation completes (`didActivateAblyPush`), register the push-to-start token. The token is raw `Data`, exactly as delivered by ActivityKit; completion is reported through the `didUpdateAblyPush` delegate callback:
+
+
+```swift
+Task {
+ for await token in Activity.pushToStartTokenUpdates {
+ rest.push.registerPushToStartToken(token)
+ }
+}
+```
+
+
+Finally, make the device targetable for push-to-start. Either subscribe it to an Ably channel, which is only used to select which devices receive push-to-start and is unrelated to the APNs broadcast channel that carries updates:
+
+
+```swift
+rest.channels.get("games:lal-bos").push.subscribeDevice { error in
+ // Subscribed: the server can now target this channel
+}
+```
+
+
+or use the device's Ably `deviceId` (available as `rest.device.id` after activation) to target it directly.
+
+## Create a broadcast channel
+
+On the server, create the APNs broadcast channel through Ably's push admin API:
+
+
+```javascript
+const rest = new Ably.Rest({ key: process.env.ABLY_API_KEY });
+
+const { id, apnsChannelId } = await rest.push.admin.createApnsBroadcast({
+ messageStoragePolicy: 1,
+});
+```
+
+
+The underlying REST endpoint is `POST /push/apnsBroadcastChannels`.
+
+Keep both identifiers: `id` drives every later server call, and `apnsChannelId` is what devices subscribe to. `messageStoragePolicy: 1` tells APNs to cache the most recent broadcast for late joiners.
+
+## Start a Live Activity
+
+### Push-to-start from the server
+
+Start a Live Activity on every device subscribed to the given Ably channels, or on a specific device by `deviceId`. Ably uses each matched device's registered push-to-start token, and `input-push-channel` subscribes the new activity to the broadcast channel:
+
+
+```javascript
+await rest.push.admin.liveActivity.start({
+ recipient: { channels: ['games:lal-bos'] }, // or { deviceId: '...' }
+ apnsBroadcast: id,
+ apns: {
+ aps: {
+ timestamp: Math.floor(Date.now() / 1000),
+ event: 'start',
+ 'input-push-channel': apnsChannelId,
+ 'content-state': {
+ homeScore: 0,
+ awayScore: 0,
+ gameStatus: 'scheduled',
+ period: 'Q1',
+ clock: '12:00',
+ lastPlay: 'Tip-off soon',
+ },
+ 'attributes-type': 'GameAttributes',
+ attributes: { homeTeam: 'Lakers', awayTeam: 'Celtics' },
+ alert: {
+ title: 'Lakers vs Celtics',
+ body: 'Game starting!',
+ },
+ },
+ },
+ headers: { 'apns-priority': '10' },
+});
+```
+
+
+The underlying REST endpoint is `POST /push/apnsBroadcastChannels/{id}/start`.
+
+### Start locally in the app
+
+Alternatively, the app starts the activity itself and subscribes it to the broadcast channel with `pushType: .channel`:
+
+
+```swift
+let activity = try Activity.request(
+ attributes: GameAttributes(homeTeam: "Lakers", awayTeam: "Celtics"),
+ content: ActivityContent(state: initialState, staleDate: nil),
+ pushType: .channel(apnsChannelId)
+)
+```
+
+
+## Update and end
+
+Each update is one broadcast against the Ably broadcast `id`; APNs delivers it to every subscribed activity:
+
+
+```javascript
+await rest.push.admin.liveActivity.update({
+ apnsBroadcast: id,
+ apns: {
+ aps: {
+ timestamp: Math.floor(Date.now() / 1000),
+ event: 'update',
+ 'content-state': {
+ homeScore: 102,
+ awayScore: 98,
+ gameStatus: 'live',
+ period: 'Q4',
+ clock: '2:14',
+ lastPlay: 'Curry 3PT (25 PTS)',
+ },
+ },
+ },
+ headers: {
+ 'apns-priority': '10',
+ 'apns-expiration': String(Math.floor(Date.now() / 1000) + 3600),
+ },
+});
+```
+
+
+The underlying REST endpoint is `POST /push/apnsBroadcastChannels/{id}/broadcast`: an update is a single broadcast to the channel.
+
+Ending works the same way with `event: 'end'`. An optional `dismissal-date` controls when iOS removes the activity from the lock screen:
+
+
+```javascript
+await rest.push.admin.liveActivity.end({
+ apnsBroadcast: id,
+ apns: {
+ aps: {
+ timestamp: Math.floor(Date.now() / 1000),
+ event: 'end',
+ 'content-state': {
+ homeScore: 112,
+ awayScore: 104,
+ gameStatus: 'finished',
+ period: 'Final',
+ clock: '',
+ lastPlay: 'Final',
+ },
+ 'dismissal-date': Math.floor(Date.now() / 1000) + 3600,
+ },
+ },
+ headers: { 'apns-priority': '10' },
+});
+```
+
+
+The underlying REST endpoint is `POST /push/apnsBroadcastChannels/{id}/end`.
+
+## Payload reference
+
+Ably passes the `apns` payload to APNs as-is. The following `aps` fields are used by Live Activities:
+
+| **Field** | **Used in** | **Description** |
+| --- | --- | --- |
+| `timestamp` | all | Unix epoch seconds; APNs discards updates older than the activity's current state. |
+| `event` | all | `start`, `update`, or `end`. |
+| `content-state` | all | The activity's dynamic state; keys must match the Swift `ContentState` properties. |
+| `attributes-type` | start | The exact name of the Swift `ActivityAttributes` struct. |
+| `attributes` | start | The activity's fixed attributes; `Date` values as Unix epoch seconds. |
+| `input-push-channel` | start | The `apnsChannelId` the started activity subscribes to for broadcast updates. |
+| `alert` | start, update | The user-visible notification shown when the activity starts, or when an update is significant enough to alert the user. |
+| `dismissal-date` | end | Unix epoch seconds after which iOS removes the ended activity from the lock screen. |
+
+Supported request `headers`:
+
+| **Header** | **Description** |
+| --- | --- |
+| `apns-priority` | `10` for immediate delivery, `5` for opportunistic delivery. |
+| `apns-expiration` | Unix epoch seconds until which APNs stores the broadcast for delivery. |