Skip to content
Shiny.NET
Shiny MAUI Shell v7 - App Links, App Shortcuts, & Navigation Interception!Shortcut me to it

Push Tokens & Server Updates

A Live Activity is most useful when a server drives it — the app is usually suspended or closed for the part of the story the user actually cares about. iOS supports this through APNs; Android does not have the concept and is updated by your own app instead, typically from an FCM data message handled by Shiny.Push.

Three kinds of token, and they are not interchangeable

Section titled “Three kinds of token, and they are not interchangeable”

This is where most Live Activity integrations go wrong. Apple issues tokens that look like device tokens but are only valid for the liveactivity topic.

Token Lifetime PushTokenKind Where you get it
Device token the install Device Shiny.Push
Push-to-start (iOS 17.2+) the install — valid before any activity exists LiveActivityStart OnPushToStartTokenChanged
Per-activity update born and dead with that one activity LiveActivityUpdate OnPushTokenChanged

A 410 Unregistered on an update token is normal and expected — it means that activity has ended.

Tokens are not available synchronously — never poll PushToStartToken. The delegate is resolved from DI, so it can take dependencies, and it runs even when the event arrives with the app backgrounded or freshly relaunched.

public class MyLiveActivityDelegate(IMyApi api) : LiveActivityDelegate
{
// the device's push-to-start token — survives launches, lets the server start an activity
// with the app closed (iOS 17.2+)
public override Task OnPushToStartTokenChanged(string token)
=> api.RegisterToken(token, PushTokenKind.LiveActivityStart);
// this one activity's own token — the only way to update it remotely, and it dies with it
public override Task OnPushTokenChanged(LiveActivity activity, string token)
=> api.RegisterActivityToken(activity.Id, token);
public override Task OnStarted(LiveActivity activity) => Task.CompletedTask;
public override Task OnStateChanged(LiveActivity activity) => Task.CompletedTask;
}
builder.Services.AddLiveActivities<MyLiveActivityDelegate>();

LiveActivityDelegate is a no-op base class — inherit it and override only what you need. Most apps only care about the two token callbacks.

Set RequestPushToken = false on LiveActivityRequest if a particular activity is app-driven and does not need a token.

Shiny.Extensions.Push builds the three lifecycle pushes and enforces the fields each one requires — Live Activity pushes use a different push type, topic and aps body from an ordinary alert.

// Start — push-to-start, iOS 17.2+. Goes to a LiveActivityStart token.
await pushManager.SendLiveActivity(
LiveActivityPush.Start(
attributesType: "ShinyActivityAttributes",
attributes: new Dictionary<string, LiveActivityValue> { ["orderNumber"] = "A-1234" },
contentState: state,
alertTitle: "Your order is on the way"
),
new PushFilter { UserIdentifier = "user-42" }
);
// Update — goes to that activity's own token.
await pushManager.SendLiveActivityToTokens([activityToken], LiveActivityPush.Update(state));
// End — optional final state, and when it should leave the Lock Screen.
await pushManager.SendLiveActivityToTokens(
[activityToken],
LiveActivityPush.End(finalState, dismissalDate: DateTimeOffset.UtcNow.AddMinutes(5))
);

See Live Activities in Shiny.Extensions.Push for the full server story.

LiveActivityContentSchema is public so a server payload can be built — or verified — against exactly the shape the app produces:

{
"title": "Out for delivery",
"body": "2 stops away",
"shortStatus": "5 min",
"progress": 0.65,
"progressStart": 774835200.0,
"progressEnd": 774838800.0,
"indeterminate": false,
"data": { "orderId": "A-1234" }
}

Keep the payload small: ActivityKit caps content-state at 4KB. Data is deliberately string-keyed and string-valued so the payload is byte-identical whether it came from your app or from your server.

Both token properties are null on Android — there is no push-token concept for a live update. Update the activity from your own app, typically in an FCM data message handler registered through Shiny.Push, by calling ILiveActivityManager.Update with the same LiveActivityContent.