Skip to main content
Read the age range the operating system already holds for your user, without asking anyone for a birthdate. iOS and Android both answer with an age band, 13 to 15, 16 to 17, 18 and over, plus how that age was established, so you can gate mature content, unlock adult-only features, or soften an experience for a minor from a single call. The same code works on both platforms and returns the same shape, and on a build or device that cannot answer it says so instead of guessing.
Age assurance is a software dependency, not a runtime toggle. The native SDKs are linked into your binary at build time, so enabling it in Despia does nothing until you ship a new build with an increased version code. It also needs configuration in the Apple and Google accounts you own. Until all three are done, every call answers status: "unavailable", which is safe to ship but is not an answer. See Enabling age assurance below.

Installation


How it works

Assign window.onAgeAssuranceResult, then fire ageassurance://check. The operating system shows its own consent sheet the first time and the verdict arrives at your callback. Nothing throws and nothing rejects: every outcome, including every failure, arrives as a verdict you read the same way.
gates is a comma-separated list of up to three ages you want answered, and it defaults to 13,16,18. Ask for the ages you actually gate on, since anything else comes back as unknown rather than as a guess. id is echoed back on the verdict as requestId so you can tell two requests apart, and accepts letters, digits, _, . and - up to 128 characters.

Reading the verdict

Branch on status first. It tells you what kind of answer you got, and ok is a shorthand for the two statuses where you learned nothing about the user.
object
The user shared a band. Carries ageLower and ageUpper as whole years, either of which can be null for an open-ended band, plus assurance and a computed gates map. This is the only status where gates can answer anything other than null.
object
The user saw the sheet and chose not to share. ok is true because this is a real answer, not a failure. Every gate is null and there is no band. Fall back to your own gating.
object
Android only. Google holds no usable signal until the user completes verification with Play. Fire ageassurance://resolve to send them into that flow.
object
iOS only. Apple reports this user is not subject to age checks at all, which is knowledge rather than absence. Treat it as an adult account unless your own policy says otherwise.
object
No provider could answer. ok is false and error.code says why: os_unsupported, not_configured, no_play_services, no_result, or a Play condition this install cannot recover from. Use your own gating.
object
The call itself failed. ok is false. error.code is stable and machine-readable, error.message is advisory text for your logs. Common codes are invalid_gates, too_many_gates, unsupported_action, network_error, and timeout.

Awaiting a verdict instead of a callback

Name the global you want read back and the call resolves once the verdict lands there. Every answer is written to window.ageAssuranceVerdict, so that is the key to watch.
Use this for the calls that never prompt, ageassurance://last and ageassurance://forget. The watch gives up after 30 seconds, and a check on a cold cache waits on a human reading a system consent sheet, which can take longer than that. Anything that prompts belongs on the callback above, which fires whenever the answer arrives no matter how long the user takes. Fire one command at a time and wait for the answer before issuing the next.

Reading a gate without under-gating a minor

Each key in gates is one of three values, and the third one is the point of the whole design. true means the band’s floor clears that age. false means its ceiling falls below it. null means the age falls inside the band and the operating system genuinely did not answer that question.
Test for === true before you unlock anything, and never treat null as false or as true. A user in a 13 to 16 band asked about 15 gets null, and reading that as “not old enough” over-restricts them while reading it as “old enough” hands adult content to a minor. That second mistake is the one that costs an app its store listing. Keys serialize as strings, so use verdict.gates["18"], and note that a gate falling between two of your configured Android bands can only ever answer null.

Knowing how the age was established

assurance tells you how much weight the answer carries, so you can require a stronger signal for the things that need one.

Checking availability before you prompt

Branch on what the build reports, never on the operating system version or the user agent. These are plain globals, so reading them costs nothing and never prompts. The SDK proxies property reads through to window, so despia.ageAssuranceAvailable and window.ageAssuranceAvailable are the same value.
ageAssuranceFeatures names the provider-backed hosts, so forget is not in it: it works on every build, with or without a provider. These globals exist only in your top-level page. An <iframe> never receives them, and an ageassurance:// call from one is refused without prompting. A framed widget that needs the verdict gets it from the top-level page through postMessage.

Clearing the verdict on logout

The verdict is held for the life of the app process, which is what stops a page reload prompting the user again. Nothing expires it, so on logout or an account switch, clear it yourself.
That drops the retained verdict on the native side and sets ageAssuranceVerdict back to null. It works with or without a provider present and it never prompts. Without it, the next person to sign in on that device can read the previous user’s age range.

Handling verification on Android

Google can answer verification_required, meaning the user has to complete verification with Play before any signal exists. Firing ageassurance://resolve re-runs the flow and lets Play present its own verification screens.
There is no iOS counterpart, because a declined range on iOS is simply declined and there is nowhere to send the user. On iOS resolve is absent from ageAssuranceFeatures and firing it answers error with unsupported_action, so test ageAssuranceFeatures before you offer the option. The callback above re-enters on the second verdict, so guard on status to avoid looping.

Enabling age assurance

Age assurance links native SDKs into your app, so it is enabled at build time rather than at runtime. The toggle, the store-side configuration, and a fresh build are all required before any real signal arrives.
1

Enable the addon in Despia

Open your app in the Despia Editor, go to App, then Addons, and turn on Age Assurance.
2

Enable the Apple capability

Open Certificates, Identifiers and Profiles, select the identifier matching your app’s bundle ID, and enable Declared Age Range under Capabilities. If it is not in the list, open the Capability Requests tab on the same identifier and request it.
3

Regenerate your provisioning profile

Open Profiles, edit the profile your app builds with, save it, and download the result. A profile created before the capability existed does not carry it, and the build switches the feature off rather than failing to sign.
4

Accept the Play Age Signals terms

In Google Play Console, open your app, then Policy and programs, then App content. Find Age signals and accept the terms.
5

Choose your Android age bands

On the same Age signals page, set the minimum ages your app needs, up to three, for example 13, 16 and 18. Match them to the ages you actually gate on: a gate that falls between two configured bands can only ever answer null.
6

Rebuild with an increased version code

Increase your version code, then trigger a fresh build in Despia and ship it. The native SDKs are compiled into the binary, so the addon has no effect on any build made before you turned it on.
Turning the addon on without shipping a new build changes nothing, and it fails silently. Your existing app keeps answering status: "unavailable" with reason: "not_configured", with no error and no warning anywhere, because the code that talks to Apple and Google is not in that binary. The same is true if you reuse a version code: the store rejects or ignores the upload and your users stay on the old build. Increase the version code, rebuild, and confirm on the new install that despia.ageAssuranceAvailable is true before you rely on any verdict.

Coverage on each platform

Below iOS 26.2 there is no such API to call, so available is false with reason: "os_unsupported" and your own gating has to carry the user. The iOS Simulator always answers unavailable, so testing the real flow needs a physical device on iOS 26.2 or later. Android reaches effectively every device you ship to, which makes it the platform where this feature does most of its work today. Because Apple shapes its band from the ages you pass at call time and Google returns bands you configured in Play Console, the same user can produce different ageLower and ageUpper values on each platform. The gates map is the part that means the same thing everywhere, which is why it is what you should branch on.

Verifying the result on your server

Everything your web app receives is client-side and can be forged by anyone who can run code in the page. Treat the verdict as a signal that shapes the experience, never as proof.
For anything load-bearing, a legal obligation, a payment, or a content restriction you would have to defend, echo the verdict to your backend and pair it with a platform attestation, App Attest on iOS or Play Integrity on Android. That is what tells your server the verdict came from a genuine install of your app rather than from a script.

Resources

NPM Package

despia-native