Skip to content
Shiny.NET

Live Activities

NuGet package Shiny.AppDeviceBridge.LiveActivities

The live activities bridge lets the page put an ongoing status on the device: a delivery, a timer, a score. On iOS it’s a Live Activity on the Lock Screen and in the Dynamic Island. On Android 16 it’s a Live Update, a promoted ongoing notification that also shows as a status bar chip. It’s built on Shiny.Mobile.LiveActivities 5.9. The page decides what an activity says; the app’s widget decides how it looks.

Platform Backed by
iOS, iPadOS 16.2+ ActivityKit, rendered by a WidgetKit extension built into the app (see below)
Android a promoted ongoing notification on API 36+; an ordinary progress notification before that
Mac Catalyst, macOS, Windows, Linux, iOS before 16.2 nothing: every route answers 501
Terminal window
dotnet add package Shiny.AppDeviceBridge.LiveActivities
builder.UseAppDeviceBridge(
bridge => bridge.AddLiveActivitiesBridge(),
webApp => { /* … */ }
);

AddLiveActivitiesBridge() registers Shiny’s live activity service with the bridge’s delegate, so there’s nothing else to call. Its optional argument is Shiny’s LiveActivityOptions, which names the Android notification channel (o.ChannelName = "Deliveries"). An app that already called services.AddLiveActivities(…) keeps its registration, and the bridge only adds its delegate.

iOS needs the widget extension. ActivityKit draws an activity from a SwiftUI widget in the app bundle. Turn on Shiny’s, and the iOS build compiles it into the app and adds NSSupportsLiveActivities to Info.plist:

<ShinyLiveActivityWidget Condition="$(TargetFramework.Contains('-ios'))">true</ShinyLiveActivityWidget>

Without it, starting an activity succeeds and nothing appears. The stock widget shows the title, body, short status and progress. To draw your own, see the iOS widget extension. On a device build the extension (<bundle id>.LiveActivity) needs a provisioning profile, which a wildcard development profile covers.

// Program.cs (Blazor WebAssembly)
builder.Services.AddWebAppHostClient().AddLiveActivitiesBridgeClient();
@inject ILiveActivitiesBridge Activities
var activity = await Activities.StartAsync(new LiveActivityStartRequest(
new LiveActivityContent(Title: "Out for delivery", Body: "Order #4821", ShortStatus: "12 min", Progress: new LiveActivityProgress(Value: 0.5)),
Attributes: new Dictionary<string, string> { ["order"] = "4821" }
));
await Activities.UpdateAsync(activity.Id, new LiveActivityUpdateRequest(
new LiveActivityContent(Title: "Arriving now", ShortStatus: "1 min", Progress: new LiveActivityProgress(Value: 0.95)),
new LiveActivityAlert("Your order is here")
));
await Activities.EndAsync(activity.Id, new LiveActivityEndRequest(new LiveActivityContent(Title: "Delivered"), DateTimeOffset.Now.AddMinutes(5)));
import { LiveActivitiesBridge } from "@shinyorg/appdevicebridge";
const activities = new LiveActivitiesBridge();
const { id } = await activities.start({ content: { title: "Out for delivery", shortStatus: "12 min", progress: { value: 0.5 } } });
await activities.update(id, { content: { title: "Arriving now", progress: { value: 0.95 } }, alert: { title: "Your order is here" } });
await activities.end(id, { content: { title: "Delivered" } });

Every route, its body and its response is listed under Requests.

  • Content is title, body, shortStatus (a few characters, for the compact Dynamic Island and the Android status bar chip), progress ({ value } from 0 to 1, { start, end } for a timer the system advances itself, or { indeterminate: true }), staleDate, relevanceScore and data, strings for your own widget to read.
  • Attributes never change for the activity’s lifetime. kind picks a layout when your widget has several.
  • Errors: 501 not_supported, 404 for an id that isn’t running, 400 for a body without content or a progress value outside 0–1, and 502 live_activity_failed when the system refuses — the user switched live activities off, or the app has too many running.

A server can update an activity, or start one with the app closed, through APNs. It needs the tokens iOS issues: one per activity (when requestPushToken is true, the default) and one push-to-start token per device (iOS 17.2+).

A push token needs the aps-environment entitlement. Without it, iOS refuses to start an activity that asks for a token, and the bridge answers 502 live_activity_failed (ActivityKit says only “ActivityInput error 0”). An app that doesn’t use push starts its activities with requestPushToken: false. They arrive as events and handler calls. A handler runs in the page if one is listening, and otherwise in background.js, so a token issued with no page open still reaches your server.

background.js
appdevicebridge.on("liveactivities.token", async ({ activityId, token }) => {
await fetch("https://api.example.com/live-activity-tokens", { method: "POST", body: JSON.stringify({ activityId, token }) });
});
appdevicebridge.on("liveactivities.starttoken", async ({ token }) => {
await fetch("https://api.example.com/push-to-start", { method: "POST", body: JSON.stringify({ token }) });
});
Handler and event Payload
liveactivities.started { id, state, pushToken }, from the page or a push-to-start notification
liveactivities.state { id, state, pushToken } when one turns Stale, Ended or Dismissed
liveactivities.token { activityId, token }
liveactivities.starttoken { token }

The Shiny Live Activities push docs cover the APNs payloads. A push-to-start payload names the same attributes the page starts an activity with.

GET/_bridge/liveactivities#

Whether the user allows live activities, and the device's push-to-start token when it has one.

C#ILiveActivitiesBridge.GetStatusAsyncTSLiveActivitiesBridge.getStatus
Response
200 LiveActivitiesStatus
LiveActivitiesStatus · 2 fields
accessAccessStateThe iOS Live Activities switch for this app, or notification permission on Android.
pushToStartTokenstring | nullThe device's push-to-start token (iOS 17.2+); null on Android, older iOS, and until the system issues one.
POST/_bridge/liveactivities/access#

Asks for the permission live activities need — notification permission on Android. iOS has no prompt, so it reports the Live Activities switch in Settings.

C#ILiveActivitiesBridge.RequestAccessAsyncTSLiveActivitiesBridge.requestAccess
Response
200 LiveActivitiesStatus
LiveActivitiesStatus · 2 fields
accessAccessStateThe iOS Live Activities switch for this app, or notification permission on Android.
pushToStartTokenstring | nullThe device's push-to-start token (iOS 17.2+); null on Android, older iOS, and until the system issues one.
GET/_bridge/liveactivities/activities#

The activities this app has running, newest first.

C#ILiveActivitiesBridge.GetActivitiesAsyncTSLiveActivitiesBridge.getActivities
Response
200 LiveActivity[]
LiveActivity · 3 fields
idstringThe system's id, which updates and ends it.
stateLiveActivityStateWhere it is in its life.
pushTokenstring | nullThe token that addresses it from a server, once the system issued one (iOS).
POST/_bridge/liveactivities/activities#

Starts an activity. Fails with 502 (live_activity_failed) when the system refuses it — the user switched live activities off, or the app has too many running.

C#ILiveActivitiesBridge.StartAsyncTSLiveActivitiesBridge.start
Body
application/json · LiveActivityStartRequest
LiveActivityStartRequest · 4 fields
contentLiveActivityContentWhat it says first.
attributesRecord&lt;string, string&gt; | nullWhat never changes for its lifetime — an order number, the teams. A push-to-start payload names the same attributes.
kindstring | nullWhich layout renders it, for an app whose widget has more than one.
requestPushTokenbooleanAsk for a push token so a server can update it; it arrives as liveactivities.token. On iOS the app needs the aps-environment entitlement for this, or the start fails.
Response
200 LiveActivity
LiveActivity · 3 fields
idstringThe system's id, which updates and ends it.
stateLiveActivityStateWhere it is in its life.
pushTokenstring | nullThe token that addresses it from a server, once the system issued one (iOS).
Errors
502Upstream failed
PUT/_bridge/liveactivities/activities/{id}#

Replaces a running activity's content. Fails with 404 when no running activity has the id.

C#ILiveActivitiesBridge.UpdateAsyncTSLiveActivitiesBridge.update
ParameterInType
idpathstringrequired
Body
application/json · LiveActivityUpdateRequest
LiveActivityUpdateRequest · 2 fields
contentLiveActivityContentReplaces what it says.
alertLiveActivityAlert | nullAlert the user about the change; silent when omitted.
Response
204 No content
Errors
404Not found
POST/_bridge/liveactivities/activities/{id}/end#

Ends an activity, optionally showing final content until it is dismissed. Fails with 404 when no running activity has the id.

C#ILiveActivitiesBridge.EndAsyncTSLiveActivitiesBridge.end
ParameterInType
idpathstringrequired
Body
application/json · LiveActivityEndRequest
LiveActivityEndRequest · 2 fields
contentLiveActivityContent | nullFinal content to show until it is dismissed; the last content when omitted.
dismissAtDate | string | nullWhen to take it off screen; the system decides when omitted, and a past time removes it now.
Response
204 No content
Errors
404Not found
DELETE/_bridge/liveactivities/activities#

Ends every activity this app has running.

C#ILiveActivitiesBridge.EndAllAsyncTSLiveActivitiesBridge.endAll
Response
204 No content
EVENTliveactivities.started#

An activity started — from this app, or from a push-to-start notification. Also handed to a liveactivities.started handler.

C#ILiveActivitiesBridge.OnStartedAsyncTSLiveActivitiesBridge.onStarted
Payload
LiveActivity
idstringThe system's id, which updates and ends it.
stateLiveActivityStateWhere it is in its life.
pushTokenstring | nullThe token that addresses it from a server, once the system issued one (iOS).
EVENTliveactivities.state#

An activity became stale, ended or was dismissed. Also handed to a liveactivities.state handler.

C#ILiveActivitiesBridge.OnStateChangedAsyncTSLiveActivitiesBridge.onStateChanged
Payload
LiveActivity
idstringThe system's id, which updates and ends it.
stateLiveActivityStateWhere it is in its life.
pushTokenstring | nullThe token that addresses it from a server, once the system issued one (iOS).
EVENTliveactivities.token#

The system issued or rotated the push token for one activity (iOS). Send it to your server: it is the only way to update that activity remotely. Also handed to a liveactivities.token handler, so background.js can send it when no page is open.

C#ILiveActivitiesBridge.OnPushTokenAsyncTSLiveActivitiesBridge.onPushToken
Payload
LiveActivityPushToken
activityIdstringThe activity it addresses.
tokenstringThe APNs token, hex.
EVENTliveactivities.starttoken#

The device's push-to-start token appeared or rotated (iOS 17.2+). A server uses it to start an activity while the app is not running. Also handed to a liveactivities.starttoken handler.

C#ILiveActivitiesBridge.OnPushToStartTokenAsyncTSLiveActivitiesBridge.onPushToStartToken
Payload
LiveActivityPushToStartToken
tokenstring

JSON property names are camelCase and enums travel as their names. A ? marks a property the payload may omit;| null one that may be null.

LiveActivitiesStatusobject#

Whether this app can show live activities.

accessAccessStateThe iOS Live Activities switch for this app, or notification permission on Android.
pushToStartTokenstring | nullThe device's push-to-start token (iOS 17.2+); null on Android, older iOS, and until the system issues one.
AccessStateenum#

Where a permission stands. Reported by every bridge that needs one; the names match Shiny's own.

"Unknown""NotSupported""NotSetup""Disabled""Restricted""Denied""Available"
LiveActivityobject#

A running, or recently ended, live activity.

idstringThe system's id, which updates and ends it.
stateLiveActivityStateWhere it is in its life.
pushTokenstring | nullThe token that addresses it from a server, once the system issued one (iOS).
LiveActivityStateenum#

Where a live activity is in its life.

"Active""Stale""Ended""Dismissed"
LiveActivityStartRequestobject#

An activity to start.

contentLiveActivityContentWhat it says first.
attributes?Record&lt;string, string&gt; | nullWhat never changes for its lifetime — an order number, the teams. A push-to-start payload names the same attributes.
kind?string | nullWhich layout renders it, for an app whose widget has more than one.
requestPushToken?booleanAsk for a push token so a server can update it; it arrives as liveactivities.token. On iOS the app needs the aps-environment entitlement for this, or the start fails.
LiveActivityContentobject#

What an activity says — everything that can change while it runs.

title?string | nullThe headline: "Arriving in 5 min", the team names.
body?string | nullDetail under the title.
shortStatus?string | nullA few characters for the tightest places — the compact Dynamic Island and the Android 16 status bar chip: "5 min", "2-1".
progress?LiveActivityProgress | nullAn optional progress bar.
staleDate?Date | string | nullWhen this content goes out of date; the activity turns Stale then.
relevanceScore?number | nullRanks this activity against the app's others when several run (iOS).
data?Record&lt;string, string&gt; | nullStrings for the app's own widget to read.
LiveActivityProgressobject#

A progress bar. Set value, or start and end for a timer the system advances itself, or indeterminate.

value?number | nullThe completed fraction, 0 to 1.
start?Date | string | nullThe start of a time range the system animates between.
end?Date | string | nullThe end of that range.
indeterminate?booleanProgress of unknown length.
LiveActivityUpdateRequestobject#

New content for a running activity.

contentLiveActivityContentReplaces what it says.
alert?LiveActivityAlert | nullAlert the user about the change; silent when omitted.
LiveActivityAlertobject#

Turns an update into a banner, and a tap on a paired watch, instead of a silent refresh.

titlestring
body?string | null
LiveActivityEndRequestobject#

How an activity ends.

content?LiveActivityContent | nullFinal content to show until it is dismissed; the last content when omitted.
dismissAt?Date | string | nullWhen to take it off screen; the system decides when omitted, and a past time removes it now.
LiveActivityPushTokenobject#

An activity's push token.

activityIdstringThe activity it addresses.
tokenstringThe APNs token, hex.
LiveActivityPushToStartTokenobject#

The device's push-to-start token.

tokenstring

The live activities routes sit under AppDeviceBridgePolicies.Bridges like every other bridge: callers on this device by default. This bridge doesn’t change the default policy, the host-name check or the launch session. See Security.