Documentation
Age Verification
Read the person's age range from iOS (Declared Age Range) and Google Play (Age Signals)
Turn on Age verification in the dashboard (Compliance tab) and your app gets the age range the operating system already knows about the person: Apple’s Declared Age Range on iOS and Google’s Play Age Signals on Android. You receive an age range (for example 18+ or 13–15), never a birthdate, so you can adapt your website for minors or keep an 18+ service adults-only without collecting ID yourself.
Before you start
- Turn on Age verification in the Compliance tab, choose up to 3 age gates (e.g.
18) and rebuild the app. - iOS: enable the Declared Age Range capability on your App ID (
<your-bundle-id>) once, in the Apple Developer Portal → Identifiers → your App ID → Capabilities → Declared Age Range → Save. Apple doesn’t allow tools to turn it on for you; the build stops with these steps if it’s missing. - iOS: signals need iOS 26 or later. Older iPhones answer
UNAVAILABLE. - Android: Google Play only returns signals where the law requires it (currently Brazil and Texas, more US states to follow). Everywhere else the answer is
UNAVAILABLE.
How it works
- When the app starts, it asks the OS for the age range using your age gates before loading your website. iOS may show its own sharing sheet (usually only the first time); Google Play may show its prompt.
- If you enabled the optional fallback modal, people with no OS signal see a simple "I’m 18 or older" dialog instead.
- Your website reads the result from
window.WebToApp.Compliance, on every page. - At any time (for example at sign-up) your website can ask again with
requestAgeSignal().
Read the age signal
The result is available as soon as the page loads, and every update fires a WebToAppAgeSignal event:
function handleAgeSignal(signal) {
if (signal.status === 'PENDING') return; // the app hasn't answered yet
const isMinor = signal.upperBound !== null && signal.upperBound < 18;
const isAdult = signal.lowerBound !== null && signal.lowerBound >= 18;
if (isMinor) {
showAdultsOnlyScreen(); // or turn on your minor-safe experience
} else if (isAdult) {
// adult according to the OS
} else {
// no signal (declined, unavailable…): use your own age check
}
}
if (window.WebToApp && window.WebToApp.Compliance) {
handleAgeSignal(window.WebToApp.Compliance.getAgeSignal());
}
window.addEventListener('WebToAppAgeSignal', (event) => handleAgeSignal(event.detail));window.WebToApp.Compliance only exists inside the app with Age verification on. In a normal browser, keep your website’s own age check.
Ask again (on demand)
Request a fresh signal when it matters, for example right before creating an account. It resolves with the same object as getAgeSignal():
const signal = await window.WebToApp.Compliance.requestAgeSignal([18]);
// or, without the helper:
window.FlutterWebView.postMessage(JSON.stringify({ type: 'requestAgeSignal', ageGates: [18] }));
// → answer arrives on the 'WebToAppAgeSignal' eventageGates is optional (defaults to the gates in the dashboard). Up to 3 ages; iOS uses them to decide which range to share, Google Play uses the ranges configured in Play Console. On-demand requests never show the fallback modal, and keep the previous signal if the OS has nothing new.
Signal fields
| Field | Values | Meaning |
|---|---|---|
status | SHARED, DECLINED, VERIFICATION_REQUIRED, UNAVAILABLE, SELF_DECLARED, DENIED, PENDING | See the status table below |
lowerBound | number or null | Youngest age in the range (inclusive) |
upperBound | number or null | Oldest age in the range (inclusive). null with a lowerBound means open-ended, e.g. 18+ |
declaration | selfDeclared, guardianDeclared, confirmed, null | Who set the age: the person, a parent/guardian, or confirmed by the OS provider (ID, payment card, other checks) |
source | apple, google, fallbackModal | Where the answer came from |
platform | ios, android | Native OS of the app |
reason | string or null | Why there is no signal, for UNAVAILABLE (e.g. osVersion, notAvailable, API_NOT_AVAILABLE, disabled) |
eligibleForAgeFeatures | boolean or null | iOS 26.2+: the person is in a region where Apple applies age-assurance rules |
requiredRegulatoryFeatures | string[] or null | iOS 26.4+: e.g. declaredAgeRangeRequired, significantAppChangeRequiresParentalConsent |
parentalControls | string[] | iOS: active controls, e.g. communicationLimits |
ageRangeSource | TIER_A–TIER_D or null | Android: Google’s source tier (A self-declared, B guardian, C assessed, D ID-verified) |
Statuses
SHARED | The person (or their guardian) shared an age range: read lowerBound / upperBound. |
DECLINED | They chose not to share. Treat as "unknown", not as a minor. |
VERIFICATION_REQUIRED | Android: Google needs the person to verify their age first. |
UNAVAILABLE | No signal: older OS, region not covered, capability missing, or an error (reason says which). |
SELF_DECLARED / DENIED | Answers to the fallback modal ("I’m old enough" / "I’m under"). DENIED also stops the app from loading your website. |
PENDING | Only seen by the page before the app answers. Wait for the WebToAppAgeSignal event. |
Good practice
- The signal arrives in the browser, so a user could tamper with it. Use it to restrict (block minors, hide mature content); don’t treat it as proof to unlock something sensitive.
- Keep a fallback for
DECLINED/UNAVAILABLE: most people in most regions won’t have a signal yet. - Store only what you need, e.g. "adult per iOS on <date>", not the raw range history.
- In App Store Connect, mention in the Review Notes that the app uses Apple’s Declared Age Range API. Answer the age-rating questions about age assurance honestly: a self-declared range is not ID verification.