Technical

Understanding OpenID4VP: The Protocol Behind EU Digital Identity Wallets

A technical deep dive into OpenID4VP — the verification protocol that EUDI Wallets use to share verified attributes with businesses. How it works, why it matters, and how OpenEUDI implements it.

eIDAS Pro Team
March 22, 2026
12 min read

Corrected 20 October 2026: an earlier version of this post said that asking a wallet for age_over_18 does not reveal the birth date, implying the EUDI PID carries that attribute. It does not — from a PID the verifier receives the birth date and derives the yes/no itself; a DOB-free age_over_18 exists only in the separate proof-of-age attestation. The code samples have also been updated to the real @openeudi API. We also corrected how a request names what it wants: OpenID4VP 1.0 uses DCQL (dcql_query); Presentation Exchange 2.0 belonged to earlier drafts only. And we removed a claim that OpenEUDI ships a per-country compliance mapping — it does not. We also clarified that the wallet, not the verifier's SDK, validates the WRPAC.

What is OpenID4VP?

OpenID for Verifiable Presentations (OpenID4VP) is the protocol that enables EU Digital Identity Wallets to share verified identity attributes with businesses (called Relying Parties). It's the technical backbone of the eIDAS 2.0 identity verification flow.

When a user scans a QR code at checkout and their wallet sends back the signed claim the shop needs to answer "is this person over 18?" — that exchange happens over OpenID4VP.

The Verification Flow

The OpenID4VP flow has four actors:

  1. Issuer — The government or trusted authority that issued the credential (e.g., Austrian government issuing a national ID)
  2. Holder — The citizen with the EUDI Wallet on their phone
  3. Verifier (Relying Party) — The business requesting verification (your e-commerce store, KYC system, etc.)
  4. Trust Framework — EU Trusted Lists that establish which issuers and verifiers are legitimate

Step-by-Step Flow

1. Verifier creates an Authorization Request
   ↓
2. Request is encoded into a QR code (or deep link)
   ↓
3. Holder scans QR code with EUDI Wallet
   ↓
4. Wallet parses the request and shows what's being asked
   ↓
5. Holder reviews and approves with biometrics/PIN
   ↓
6. Wallet creates a Verifiable Presentation (VP)
   ↓
7. VP is sent to the Verifier's response endpoint
   ↓
8. Verifier validates the VP cryptographically
   ↓
9. Verified attributes are extracted and used

Two Credential Formats

EUDI Wallets support two credential formats, each with different cryptographic verification paths:

SD-JWT VC (Selective Disclosure JWT)

Used for remote verification flows (online, cross-device). The credential is a JSON Web Token with selective disclosure capabilities.

  • Format: JSON-based
  • Selective Disclosure: Hash-based salting — the issuer signs all claims, but the holder can reveal only specific ones
  • Signature: JWS (JSON Web Signature) with ES256
  • Verification: Parse JWT → reconstruct disclosures → verify hashes → validate signature → check issuer trust chain
Header.Payload.Signature~Disclosure1~Disclosure2~KeyBindingJWT

mdoc / mDL (Mobile Document)

Used for proximity verification flows (in-person, NFC, BLE). Based on ISO 18013-5.

  • Format: CBOR-based (binary)
  • Selective Disclosure: Built into the data structure — namespaces and data elements
  • Signature: COSE (CBOR Object Signing and Encryption)
  • Verification: Decode CBOR → verify COSE signature → validate device authentication → check issuer certificate chain

Both formats achieve the same goal — cryptographically proving that a trusted issuer attested to specific claims about the holder — but through fundamentally different technical paths.

Asking for Attributes: DCQL

The Authorization Request tells the wallet exactly which credential and which claims the verifier wants. OpenID4VP 1.0 expresses this with the Digital Credentials Query Language (DCQL), sent in the dcql_query parameter. (Earlier drafts of OpenID4VP used DIF Presentation Exchange 2.0 and a presentation_definition object instead; version 1.0 replaced it with DCQL.)

An age check against the PID in SD-JWT VC format asks for one claim — the birth date:

{ "credentials": [{ "id": "pid", "format": "dc+sd-jwt",
    "meta": { "vct_values": ["urn:eudi:pid:1"] },
    "claims": [{ "path": ["birthdate"] }] }] }

The same request against the mdoc PID uses the eu.europa.ec.eudi.pid.1 namespace and the birth_date element. DCQL can also offer alternatives: with credential_sets, a verifier can ask for "a proof-of-age attestation or a PID", and the wallet returns one of them.

WRPAC: The Verifier's Certificate

To make a verification request, the Relying Party needs a WRPAC (Wallet Relying Party Access Certificate). This certificate, defined in ETSI TS 119 475:

  • Identifies the Relying Party to the wallet
  • Specifies which attributes the RP is authorized to request
  • Is issued by a qualified trust service provider
  • Must be validated against EU Trusted Lists

The wallet checks the WRPAC before showing the consent screen. If the certificate is invalid, expired, or not in the trust list, the wallet refuses the request.

EU Trusted Lists

The EU maintains a hierarchical trust infrastructure:

EU List of Trusted Lists (LOTL)
  └── Country Trusted List (e.g., Luxembourg)
       └── Trust Service Provider (e.g., LuxTrust)
            └── Trust Service (e.g., WRPAC issuance)
                 └── Certificate (your WRPAC)

It is the wallet that walks this hierarchy to check that a WRPAC was issued by a legitimate provider in a recognised member state. The verifier side does the mirror-image job: the SDK checks that the credential it receives was signed by an issuer whose certificate chains to a trust anchor taken from a published list (for PIDs, the list of PID providers). This is one of the most complex parts of the implementation — the trust list format (XML/JSON) has its own parsing requirements, and lists are updated independently by each member state.

Privacy by Design

OpenID4VP enforces privacy at the protocol level:

  • Selective Disclosure: The holder chooses exactly which attributes to share. An age check against the PID discloses only the birth date — not name, address or document number — and the verifier reduces it to a yes/no. A DOB-free age_over_18 exists only in the separate proof-of-age attestation (eu.europa.ec.av.1), not in the PID.
  • No Correlation: Each verification session uses fresh cryptographic material, preventing verifiers from linking sessions.
  • Holder Consent: Every attribute request requires explicit user approval on the device.
  • Data Minimization: The protocol encourages requesting only what's needed. The wallet UI makes over-requesting visible to users.

This is fundamentally different from traditional KYC, where businesses often receive (and store) more data than necessary.

How OpenEUDI Implements This

The OpenEUDI SDK is structured in layers. The @openeudi/openid4vp package implements credential parsing with a simple API: parsePresentation(vpToken, options) automatically detects the credential format (SD-JWT VC or mDOC), validates cryptographic signatures, and returns verified claims.

import { parsePresentation } from '@openeudi/openid4vp';

// Automatically handles both SD-JWT VC and mDOC formats
const result = await parsePresentation(vpToken, {
  nonce,               // the nonce from your Authorization Request
  trustedCertificates, // issuer trust anchors (DER), e.g. from EU Trusted Lists
});
// result.valid  — signature, key binding and trust checks passed
// result.format — 'sd-jwt-vc' | 'mdoc'
// result.claims — verified, selectively disclosed attributes (e.g. the PID birth date)

The higher-level VerificationService builds on this to manage the full session lifecycle:

import { VerificationService, VerificationType } from '@openeudi/core';
import type { IVerificationMode } from '@openeudi/core';

// @openeudi/core ships DemoMode and MockMode. Real wallets need your own
// IVerificationMode: it builds the OpenID4VP request (signed with your WRPAC)
// and verifies the response with @openeudi/openid4vp.
declare const productionMode: IVerificationMode;

const service = new VerificationService({ mode: productionMode });

const session = await service.createSession({ type: VerificationType.AGE });
// session.walletUrl — render as a QR code or deep link

service.on('session:verified', (session, result) => {
  // result.ageVerified — a derived boolean; the birth date is not part of the result
});

Internally, the service and its verification mode:

  1. Construct a spec-compliant Authorization Request
  2. Encode it into a QR code (generateQRCode from @openeudi/core/qr)
  3. Accept the wallet's VP through service.handleCallback(), called from your response endpoint
  4. Delegate to @openeudi/openid4vp to parse and validate the VP (SD-JWT VC or mDOC)
  5. Check the issuer's certificate against EU Trusted Lists
  6. Reduce the disclosed claims to the policy result (ageVerified, countryVerified) — a birth date is used for the check, not returned
  7. Expire sessions after their TTL (five minutes by default)

All of this happens behind service.createSession(), service.handleCallback() and the session:verified event.

Cross-Country Compliance

Each EU member state has some discretion over which attributes can be requested and under which conditions. For example:

  • Age verification thresholds vary by product category and country
  • Some countries restrict certain attribute disclosures
  • National wallet implementations may use different credential schemas

Before you build a request, check what the credential actually carries: in every PID, birth_date and nationality are mandatory, while resident_country is optional and may be missing.

What's Next for the Protocol

OpenID4VP 1.0 is a final OpenID Foundation specification, and DCQL is its query language — Presentation Exchange is not part of it. The work now happens around the core protocol:

  • Credential status — checking whether a credential has been revoked
  • Cross-device flow improvements — better UX for mobile-to-desktop verification
  • Alternatives in one request — DCQL credential_sets, for example "proof-of-age attestation or PID"

OpenEUDI ships protocol changes as new versions of its npm packages.


OpenEUDI implements OpenID4VP for the EUDI Wallet ecosystem. The SDK is Apache-2.0-licensed and free to use. For production deployments with managed WRPAC certificates, see eIDAS Pro's managed plans.

Related Articles

Share this article

Help others learn about eIDAS verification