Protocols
EU age verification profile (Annex A)
The EU age verification profile, published as Annex A of the age verification technical specification, is the set of rules the EU age verification app follows. It is a profile of OpenID4VP, like HAIP, but it makes different choices at almost every decision point, so a verifier written for the EU Digital Identity Wallet alone will not satisfy it.
Also called: AV Profile, EU age verification app, age verification app, Annex A, av://
At a glance
- Defined in
- Annex A of the EU age verification technical specification
- Profiles
- OpenID4VP 1.0, with ISO mdoc as the only credential format
- Signed request
- No. Plain query parameters, JAR explicitly not used
- Reader authentication
- Out of scope, so no access certificate is needed
- Response mode
- direct_post, unencrypted
- Client identifier
- redirect_uri: followed by the response URI, so per session
- App invocation
- av:// at minimum, with the Digital Credentials API preferred
Four differences, none of them cosmetic
The first is the one that surprises people. There is no signed request object. A HAIP verifier wraps its request in a JWT, signs it, and sends the chain along in the x5c header. Annex A sends plain query parameters instead, and it gives the reason: the integrity that signing buys you depends on a trust list of relying parties, that list does not exist here, so an attacker who obtained any valid certificate could substitute a request of their own. In the specification's words, "the Age Verification solution does not use JAR".
The second follows from the first. Reader authentication and client authentication are both declared out of scope, which is why this path needs no access certificate. The app is not trying to work out who you are before it answers, and the profile says so rather than leaving it implied.
The third is the response. HAIP encrypts it, using response_mode direct_post.jwt and a key the verifier advertises in client_metadata. Annex A requires plain direct_post, so there is no response key, and client_metadata is absent from the profile altogether. A verifier that sends one anyway is sending a parameter the profile never defined.
The fourth is identity. Instead of x509_san_dns, the profile requires the client identifier to be the literal string redirect_uri followed by the response URI itself, so a verifier is identified by where it asks the answer to be sent rather than by a name inside a certificate. Annex A's worked example goes further and uses a response URI carrying a per session token, which makes its client identifier change on every request. That is what the example happens to do, not something the profile requires, and the difference is worth holding on to before you build a session token into a URL you did not need to.
How the app gets invoked
Annex A requires that the app support at least the custom URL scheme av://. That is the floor rather than the ceiling: the profile's preferred transport is the W3C Digital Credentials API, and the custom scheme is what covers the case where the browser has no such API. On Android the shipping app resolves av:// directly, so a link is enough to hand a request over.
The credential it answers with is the EU age verification attestation, doctype eu.europa.ec.av.1, an mdoc that carries age_over_N booleans and nothing else. It is not the PID, and asking for age_over_18 as a plain SD-JWT claim gets you nothing, because no EU credential carries it in that shape.
Why this matters commercially, not just technically
These two profiles are usually discussed as though one were a subset of the other. They are not. A verifier that passes the OpenID Foundation conformance suite for HAIP has proved nothing about this profile, and the suite cannot test it even in principle, because the plan only offers the encrypted response mode that Annex A forbids. The reverse holds too. Evidence for one is not evidence for the other, and we say so about our own conformance results.
The practical consequence is that the two paths are at different stages for everyone, not only for us. The wallet path waits on access certificates and on member states shipping wallets. The age verification path is answerable today, because nothing in it is gated on a certificate. If a vendor tells you they are live on EU age verification, the useful follow up is which of the two they mean.
Check one yourself
Reading about it only gets you so far. These are free validators in Tessio.Labs, no signup, and nothing you paste is stored.
Why it matters
If you're writing the integration: build for this profile separately. Reusing a HAIP request and changing the doctype will not work, because the signed request, the response mode and the client identifier all differ, and the app rejects the request rather than degrading.
If you're choosing a vendor: ask which profile they have actually completed a check on, and ask for the date. Passing HAIP conformance is a real achievement and it is not an answer to this question.
Frequently asked
Do I need a wallet relying party access certificate for the age verification app?
No. The profile puts reader authentication out of scope, so the app does not check for one. You do need one for the EU Digital Identity Wallet, which is a separate path with a separate profile.
Is the age verification app the same thing as the EU Digital Identity Wallet?
No. It is a single purpose app that holds an age attestation and nothing else, and it follows its own profile. The wallet holds identity credentials and follows HAIP. Both are EU projects and they are not interchangeable.
Why does the profile refuse signed requests when signing is normally safer?
Because signing only helps if the verifier can tell whose signature to trust, and that needs a published list of relying parties. Annex A says no such list exists for this ecosystem, so a signature would give false assurance rather than real assurance.
Can a HAIP verifier answer an age verification request?
Not without changes. It will send a signed request object the app does not want, advertise a response encryption key the profile does not define, and use the wrong client identifier scheme. Each of those is enough on its own for the request to be refused.
Primary sources
This is a plain language explanation, not legal advice. Specifications and dates in this area move, so check anything you are going to rely on against the primary sources.
Keep reading
EU age verification attestation (eu.europa.ec.av.1)
Credentials
OpenID for Verifiable Presentations (OpenID4VP)
Protocols
W3C Digital Credentials API (DC API)
Protocols
Wallet-relying party access certificate (WRPAC)
EUDI infrastructure
EU trusted list and the LOTL
eIDAS trust infrastructure
Developer docs
The age verification API
Age verification API
Or skip the theory and build against it
Tessio proves someone is over 18 from their EU Digital Identity Wallet and stores no personal data. Signup is open and a test project works as soon as you create one. Tell us what you're building.