Skip to content
Shiny.NET
Commercial SupportGuaranteed response times on Shiny bug fixes, straight from the maintainers. Plans from US$1,500 a year.See the plans

Age Signals

NuGet package Shiny.AppDeviceBridge.AgeSignals

The age signals bridge asks the app store for the user’s age range, for app store accountability laws such as Texas SB 2420, and for any app that gates features by age. It’s built on Shiny.AgeSignals: one API over Apple’s Declared Age Range and Google Play Age Signals.

Platform Backed by
iOS, macOS 26+ Declared Age Range. Older versions answer, with status: "NotSupported"
Android Google Play Age Signals, for an app installed from Google Play
Mac Catalyst, Windows, Linux nothing: every route answers 501
Terminal window
dotnet add package Shiny.AppDeviceBridge.AgeSignals
builder.UseAppDeviceBridge(
bridge => bridge.AddAgeSignalsBridge(),
webApp => { /* … */ }
);

On Apple, add the com.apple.developer.declared-age-range entitlement and enable the capability for the App ID. Without it, the request never returns. On Android, the app must be installed from Google Play; a sideloaded build answers 403 app_not_owned. See Shiny.AgeSignals.

import { AgeSignalsBridge } from "@shinyorg/appdevicebridge";
const ages = new AgeSignalsBridge();
const signal = await ages.requestAgeRange({ gates: [13, 16, 18] });
if (signal.status === "Shared" && signal.lowerBound !== null && signal.lowerBound >= 18)
showAdultContent();
else if (signal.status === "VerificationRequired")
askToVerifyInPlayStore();

In Blazor, register AddAgeSignalsBridgeClient() and inject IAgeSignalsBridge. The C# AgeSignal also has IsAtLeast(age), which answers true or false when the store is sure, and null when it can’t tell (nothing shared, or a range that straddles the age), so treat null as “not verified”.

POST agesignals/request takes one to three ascending ages, the gates the returned range is bounded by. Apple may present a sheet. Google Play ignores the gates and uses the bands set in Play Console (0–12, 13–15, 16–17 and 18+ by default), so configure the same ages there.

status Meaning
Shared An age range was shared: lowerBound and upperBound (inclusive; upperBound is null for the highest band)
Declined The user, or their guardian, declined to share. It’s a result, not an error
VerificationRequired Google Play: the user is where verification is mandatory and their age is unknown. Send them to the Play Store
NotSupported The OS version has no store age API

source says how the range was established, weakest first: SelfDeclared, GuardianDeclared (a supervised account), Confirmed (a payment card, ID or age estimation) and IdVerified (Google Play only; Apple reports these as Confirmed). An app can require a minimum. communicationLimits is Apple’s flag for a child whose guardian turned on communication limits.

Nothing is cached: consent can be revoked and a birthday crosses a band, so ask at the moments the law cares about — first launch, before a gated feature, and after an update.

GET agesignals reports, with no UI, whether the OS has the API and what Apple says applies to this user: isEligible (26.2+), and ageRangeRequired and the significant-change flags (26.4+). They are null on Android.

When a significant change to the app needs a guardian’s acknowledgment, POST agesignals/significant-update with { description } shows Apple’s sheet (iOS 26.4+). It answers { shown: false } on Android: declare the change in Play Console instead, and read the guardian’s approval from the signal’s significantChange, significantChangeApprovedThrough and installId.

Status Code When
400 bad_request The gates are not one to three positive, ascending ages, or the description is empty
403 app_not_owned Google Play: the app was not installed from Play
409 invalid_account, no_user_interface The account can’t share an age range, or there is no window to present from
503 network, transient, not_available, store_unavailable Try again later, or the store can’t answer on this device
502 age_signals_failed Anything else
GET/_bridge/agesignals#

Whether this OS version has a store age API, and what the store says applies to this user, with no UI. Values the platform cannot answer are null.

C#IAgeSignalsBridge.GetEligibilityAsyncTSAgeSignalsBridge.getEligibility
Response
200 AgeSignalsEligibility
AgeSignalsEligibility · 5 fields
isSupportedbooleanThe OS version has a store age API: iOS or macOS 26+, or Android.
isEligibleboolean | nullApple (26.2+): the user is in a region or account where age features apply. Null on Android.
ageRangeRequiredboolean | nullApple (26.4+): the app must obtain the age range.
significantChangeRequiresParentalConsentboolean | nullApple (26.4+): significant changes need a guardian's consent.
significantChangeRequiresAdultNotificationboolean | nullApple (26.4+): significant changes must be acknowledged — see showSignificantUpdate.
POST/_bridge/agesignals/request#

Asks the store for the user's age range. Apple may present a sheet; Google Play shows its sharing prompt only outside the US law states. Declining is a result (Declined), not an error. Fails with 400 when the gates are not one to three positive, ascending ages, and with the store's reason (503 network, 403 app_not_owned, …) when it cannot answer.

C#IAgeSignalsBridge.RequestAgeRangeAsyncTSAgeSignalsBridge.requestAgeRange
Body
application/json · AgeRangeRequest
AgeRangeRequest · 1 field
gatesnumber[]One to three ascending ages the returned range is bounded by, such as [13, 16, 18]. Google Play ignores these and uses the bands set in Play Console, so configure the same ages there.
Response
200 AgeSignal
AgeSignal · 8 fields
statusAgeSignalStatusWhether a range was shared.
lowerBoundnumber | nullInclusive lower bound; null when not shared, and on Apple for the lowest band.
upperBoundnumber | nullInclusive upper bound; null when not shared, and for the highest band (such as 18+).
sourceAgeRangeSourceHow the range was established.
communicationLimitsbooleanApple: a guardian has communication limits turned on for this child.
significantChangeSignificantChangeStatus | nullGoogle Play: guardian approval of significant changes; null for unsupervised accounts, supervised ones with no changes recorded, and on Apple.
significantChangeApprovedThroughstring | nullGoogle Play: the effective date of the latest approved significant change.
installIdstring | nullGoogle Play: the id Play gives a supervised install, to match revoked-approval notifications on a server.
Errors
400Bad request403Forbidden503Unavailable
POST/_bridge/agesignals/significant-update#

Shows Apple's significant-update acknowledgment (iOS 26.4+), which tells a supervised child's guardian about a significant change to the app. shown is false where there is no such sheet: on Google Play, declare the change in Play Console and read significantChange instead.

C#IAgeSignalsBridge.ShowSignificantUpdateAsyncTSAgeSignalsBridge.showSignificantUpdate
Body
application/json · SignificantUpdateRequest
SignificantUpdateRequest · 1 field
descriptionstringWhat changed, shown to the guardian.
Response
200 SignificantUpdateResult
SignificantUpdateResult · 1 field
shownbooleanWhether the acknowledgment sheet was shown; false where the platform has none.

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.

AgeSignalsEligibilityobject#
isSupportedbooleanThe OS version has a store age API: iOS or macOS 26+, or Android.
isEligibleboolean | nullApple (26.2+): the user is in a region or account where age features apply. Null on Android.
ageRangeRequiredboolean | nullApple (26.4+): the app must obtain the age range.
significantChangeRequiresParentalConsentboolean | nullApple (26.4+): significant changes need a guardian's consent.
significantChangeRequiresAdultNotificationboolean | nullApple (26.4+): significant changes must be acknowledged — see showSignificantUpdate.
AgeRangeRequestobject#
gatesnumber[]One to three ascending ages the returned range is bounded by, such as [13, 16, 18]. Google Play ignores these and uses the bands set in Play Console, so configure the same ages there.
AgeSignalobject#
statusAgeSignalStatusWhether a range was shared.
lowerBoundnumber | nullInclusive lower bound; null when not shared, and on Apple for the lowest band.
upperBoundnumber | nullInclusive upper bound; null when not shared, and for the highest band (such as 18+).
sourceAgeRangeSourceHow the range was established.
communicationLimitsbooleanApple: a guardian has communication limits turned on for this child.
significantChangeSignificantChangeStatus | nullGoogle Play: guardian approval of significant changes; null for unsupervised accounts, supervised ones with no changes recorded, and on Apple.
significantChangeApprovedThroughstring | nullGoogle Play: the effective date of the latest approved significant change.
installIdstring | nullGoogle Play: the id Play gives a supervised install, to match revoked-approval notifications on a server.
AgeSignalStatusenum#

Whether an age range was shared.

"NotSupported""Declined""VerificationRequired""Shared"
AgeRangeSourceenum#

How the shared age range was established, weakest first, so an app can require a minimum.

"Unknown""SelfDeclared""GuardianDeclared""Confirmed""IdVerified"
SignificantChangeStatusenum#

Google Play: a guardian's approval of the app's significant changes.

"Approved""Pending""Declined"
SignificantUpdateRequestobject#
descriptionstringWhat changed, shown to the guardian.
SignificantUpdateResultobject#
shownbooleanWhether the acknowledgment sheet was shown; false where the platform has none.