# walt.id Documentation — Full Text
> Complete documentation of walt.id's digital identity infrastructure in one file: Part 1 covers stack-agnostic concepts, Part 2 the open-source Community Stack, Part 3 the commercial Enterprise Stack. The two stacks are distinct products with different APIs — always note which part a page belongs to.
Generated: 2026-09-03 · Community Stack version: 0.23.0 · Enterprise Stack version: 0.23.0
Page index: https://docs.walt.id/llms.txt · Stack comparison: https://docs.walt.id/community-vs-enterprise.md
OpenAPI 3.1 specifications (machine-readable):
- Issuer API — Community Stack: https://issuer.demo.walt.id/api.json
- Verifier API — Community Stack: https://verifier.demo.walt.id/api.json
- Wallet API — Community Stack: https://wallet.demo.walt.id/api.json
- Enterprise API — Enterprise Stack: https://enterprise.sandbox.walt.id/api.json
################################################################################
# PART 1 OF 3: CONCEPTS — STACK-AGNOSTIC (20 pages)
# Stack-agnostic background: DIDs, credential formats (W3C VC, SD-JWT VC, mDL/mdoc),
# selective disclosure, OID4VCI/OID4VP protocols, trust systems.
# These concepts apply to BOTH the Community Stack and the Enterprise Stack.
################################################################################
--------------------------------------------------------------------------------
PAGE: Concepts
URL: https://docs.walt.id/concepts
RAW: https://docs.walt.id/concepts.md
STACK: Concepts (stack-agnostic, applies to both Community and Enterprise Stack)
--------------------------------------------------------------------------------
# Concepts
Learn more about the general building blocks of decentralized ID solutions.
- [Decentralised Identity](https://docs.walt.id/concepts/decentralized-identity.md) - An introduction to decentralized identity.
- [ID Ecosystems](https://docs.walt.id/concepts/id-ecosystems.md) - The role of ecosystems in decentralized ID solutions.
- [Decentralised Identifiers (DIDs)](https://docs.walt.id/concepts/decentralised-identifiers.md) - The role of DIDs and how they work.
### Digital Credentials
Learn more about the different types of digital credentials
- [Verifiable Credentials (W3C)](https://docs.walt.id/concepts/digital-credentials/verifiable-credentials-w3c.md) - The W3C data model for tamper-evident digital credentials.
- [mDL / mdoc credentials](https://docs.walt.id/concepts/digital-credentials/mdoc-mdl-iso.md) - ISO/IEC 18013 mobile driver's license and mdoc credential format.
- [SD-JWT VC credentials](https://docs.walt.id/concepts/digital-credentials/sd-jwt-vc.md) - JWT-based credentials with selective disclosure for claim-level privacy.
- [Mono claim credentials](https://docs.walt.id/concepts/selective-disclosure/mono-claim-credentials) - Single-attribute credentials for minimal disclosure and simple verification.
### Credential Exchange Protocols
- [OpenID4VCI (Issuance)](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md) - Protocol for issuing verifiable credentials to wallets.
- [OpenID4VP (Presentation)](https://docs.walt.id/concepts/data-exchange-protocols/openid4vp.md) - Protocol for presenting credentials from a wallet to a verifier.
- [Digital Credentials API (Browser-Based)](https://docs.walt.id/concepts/data-exchange-protocols/dc-api.md) - W3C browser API that mediates credential presentation and issuance natively in the browser also known as DC API.
- [HAIP (High Assurance Profile)](https://docs.walt.id/concepts/data-exchange-protocols/haip.md) - Security-hardened profile of OpenID4VCI and OpenID4VP for high-assurance interoperability. Required by [eIDAS 2](https://walt.id/eidas2).
### Trust Systems
- [EU Trust Lists](https://docs.walt.id/concepts/trust-systems/eu-trust-lists.md) - Machine-readable registries of trusted entities for the EU digital identity ecosystem (eIDAS 2.0).
- [ISO mDL Trust / VICALs](https://docs.walt.id/concepts/trust-systems/iso-trust-vicals.md) - Certificate authority hierarchies for verifying ISO mobile document issuers.
- [Wallet Attestations](https://docs.walt.id/concepts/trust-systems/wallet-attestations.md) - Cryptographic proofs that establish the authenticity and integrity of digital wallets.
### Credential Lifecycle
- [Credential lifecycle](https://docs.walt.id/concepts/credential-lifecycle/credential-status-and-revocation.md) - How to manage the status (e.g. revocation) of credentials.
--------------------------------------------------------------------------------
PAGE: Decentralized Identity
URL: https://docs.walt.id/concepts/decentralized-identity
RAW: https://docs.walt.id/concepts/decentralized-identity.md
STACK: Concepts (stack-agnostic, applies to both Community and Enterprise Stack)
--------------------------------------------------------------------------------
# Decentralized Identity
## Definitions
Digital identity is the sum of all the (digital) information that exists about you. For example:
- core identity attributes (name, address or birthday),
- education and work history (diplomas, work records, certificates),
- health or insurance data (medical reports, prescriptions, vaccination passes),
- financial information (bank account information, transaction histories).
As a result, your digital identity describes who you are in every aspect of your (digital) life.
Decentralized identity is simply one (of many) approaches for making digital identity possible.
## **How decentralized identity works**
Decentralized identity enables governments and businesses to issue identity credentials to their citizens, users or other stakeholders. These credentials are stored in ID wallets and can be easily shared with others. Decentralized identity is like a 3-sided marketplace:
1. **Issuers** issue digital identity credentials (e.g. governments issue passports, universities issue diplomas).
2. **Holders** receive digital credentials (from Issuers) and store them in ID wallets. They control and share their digital credentials with third parties (Verifiers).
3. **Verifiers** rely on identity data to provide products and services, they verify and process credentials provided by Holders.
The so-called “**Trust Triangle**” illustrates these roles:

## Technology & Building Blocks
Decentralized identity is based on a set of complex technologies and protocols which can be thought of as building blocks that are available in different variations and can be put together in different ways. As a result, there are different “flavors” or ways to implement decentralized identity depending on the business requirements.
The following list outlines some of the most important ones:
- **Cryptographic keys** convey control over digital identities and enable core functionality such as encryption and authentication.
- **Decentralized Identifiers** ([DIDs](https://docs.walt.id/concepts/decentralised-identifiers.md), W3C) establish a public key infrastructure by linking keys to identifiers allowing parties to find and interact with each other.
- **Digital Credentials** like Verifiable Credentials ([VCs](https://docs.walt.id/concepts/digital-credentials/verifiable-credentials-w3c.md), W3C) and mobile driver’s license/[mdoc](https://docs.walt.id/concepts/digital-credentials/mdoc-mdl-iso.md) (ISO/IEC 18013-5/-7) are the most common “types” of digital credentials. They can be easily and securely shared with and verified by others. (Note that these credentials are never stored on a blockchain due to privacy and compliance reasons.)
- **[Selective Disclosure](https://docs.walt.id/concepts/selective-disclosure.md)** (SD) and **Zero Knowledge Proofs** (ZKPs) are ways to minimize data exposure. Selective disclosure using [SD-JWTs](https://docs.walt.id/concepts/digital-credentials/sd-jwt-vc.md) (IETF) is a solid and reliable way to improve users’ privacy that is gaining traction. While ZKPs are a promising technology with a bright future, they are not yet broadly adopted due to concerns or lacking certifications of the underlying cryptography,
- **Non-Fungible Tokens** (NFTs) and **Soulbound Tokens** (SBTs) are used to tokenize proofs or assets on blockchains. They enable “ownership-based access” such as for holders of tickets or memberships. Also, they enable on-chain proofs that can be processed by smart contracts (without the use of oracles).
- **Data Exchange Protocols** enable the transfer of identity credentials between parties (like Issuer to Holder or Holder to Verifier). Currently, OpenID Connect, the standard that already enables federated identity, is evolving into the most used standard for the exchange of digital credentials. Protocols can enable data exchange online and in-person scenarios (even offline).
- **ID Wallets** store our keys (control) and credentials (identity data). They enable the management and sharing of our identity data via easy-to-use apps.
## **Walt.id**
We’re building open source decentralized identity and wallet infrastructure trusted by thousands of developers, governments and businesses. Our goal is to enable anyone to build use cases and applications based on decentralized identity fast and with ease.
Our infrastructure enables organizations to become Issuers, Wallet Providers and Verifiers.

You can think of our infrastructure as an abstraction layer that hides technical complexity and handles different building blocks (e.g., trust registries, credential types, data exchange protocols) while ensuring ongoing compliance with standards and regulations.

## **Further Readings**
You can learn more about decentralized identity in our ebooks:
- [Decentralized Identity Playbook](https://walt.id/white-paper/decentralized-identity-playbook) (eBook)
- [Introduction to Digital Identity](https://walt.id/white-paper/digital-identity) (eBook)
- [Introduction to Decentralized Identity / Self-Sovereign Identity](https://walt.id/white-paper/self-sovereign-identity-ssi) (eBook)
- [Introduction to non-fungible (NFTs) and soulbound tokens (SBTs) for identity](https://walt.id/white-paper/nfts-for-identity) (eBook)
- [NFTs/SBTs vs Self-Sovereign Identity](https://walt.id/white-paper/ssi-vs-nfts) (eBook)
- [Monetizing decentralized identity](https://walt.id/white-paper/ssi-data-monetization) (eBook)
- [The rise of identity ecosystems](https://walt.id/white-paper/the-rise-of-identity-ecosystems) (eBook)
- [The role of blockchain for decentralized identity](https://walt.id/white-paper/the-role-of-blockchains-for-decentralized-identity) (eBook)
- [eIDAS2 is here](https://walt.id/white-paper/eidas2) (eBook in collaboration with Trustscape)
- [Me, myself and SSI](https://mvtrosz5a6vgwqrp.public.blob.vercel-storage.com/me-myself-and-ssi-bcg-x-waltid.pdf) (eBook in collaboration with BCG)
- [A digital twin for everything](https://mvtrosz5a6vgwqrp.public.blob.vercel-storage.com/digital-twin-of-everything-bcg-x-waltid.pdf) (eBook in collaboration with BCG)
--------------------------------------------------------------------------------
PAGE: ID Ecosystems
URL: https://docs.walt.id/concepts/id-ecosystems
RAW: https://docs.walt.id/concepts/id-ecosystems.md
STACK: Concepts (stack-agnostic, applies to both Community and Enterprise Stack)
--------------------------------------------------------------------------------
# ID Ecosystems
ID ecosystems enable users to control and share their data with others. They are open, user-centric systems that enable
direct interactions and the free exchange of identity information.
Identity ecosystems create trust between people and organizations that typically don’t know each other. They have two
major components: "Trust Registries" and "Governance and Trust Frameworks".
## Trust Registries
Trust Registries enable the verification of identity data, serving as the single source of truth and can be implemented
with different technologies and levels of decentralization (e.g. Domain Name Service, traditional PKIs, blockchains).
There are different types of Trust Registries for different purposes. For example:
- Organization Registries enable the verification of information about organizations acting as Issuers or Verifiers.
- Schema Registries established standards for data models and semantics.
- Revocation Registries enable management and verification of credentials’ lifecycle.
Different technologies can be used to implement Registries. For example:
- **Blockchains** or **L2s**: Blockchains are a useful technology to create Trust Registries (e.g. Organization
Registries) due to their decentralization, immutability, transparency, security, and efficiency. They provide a
permanent, auditable record without intermediaries. Also, they can be used to enable NFTs or SBTs for diverse use
cases like tokenizing proofs or assets in a way that can be processed by smart contracts. Today, we see a growing
number of developers and organizations focusing on permissioned blockchains (i.e. only a selected group can “write”).
Permissionless blockchains, like Ethereum, are still less used than permissioned alternatives particularly due to
issues with scalability, costs or the lack of customizable governance frameworks.
- **Domain Name Service (DNS)**: Considering certain drawbacks of DLTs and their relatively slow adoption by the mass
market, DNS can also be used to serve as a registry. Though it is not fully decentralized (considering its underlying
governance framework), DNS has many advantages like its maturity and global adoption. Importantly, digital identity
can be implemented without registries, particularly without blockchains, because identity data (or at least personal
data of individuals) is never anchored due to privacy and compliance reasons. However, by combining digital identity
with blockchains (or other technologies), robust and trustworthy identity ecosystems that utilize transparent DPKIs
and reliable Trust Registries can emerge.
- **Traditional Public Key Infrastructure (PKI)**: Finally, traditional public key infrastructures, that already
underpin today’s identity and certificate infrastructure can be used. The main downside is the centralization,
information asymmetry and dependence on the PKI provider.
## Governance and Trust Frameworks
Governance and Trust Frameworks are like the constitution of an ID ecosystem, ensuring trust in identity data from
Issuers and other ecosystem participants. They regulate governing bodies and processes, onboarding and accreditation,
liability and enforceability, trust and assurance levels, privacy and data protection, security and interoperability
standards. Also, these frameworks align with regulations that impact the identity industry, such as GDPR or [eIDAS2](https://walt.id/eidas2) in
Europe.
You can read more about this topic in
our [ID ecosystem ebook](https://walt.id/white-paper/the-rise-of-identity-ecosystems).
--------------------------------------------------------------------------------
PAGE: Decentralised Identifiers
URL: https://docs.walt.id/concepts/decentralised-identifiers
RAW: https://docs.walt.id/concepts/decentralised-identifiers.md
STACK: Concepts (stack-agnostic, applies to both Community and Enterprise Stack)
--------------------------------------------------------------------------------
# Decentralised Identifiers
DIDs give us the power of verifying information, for example credentials, anywhere, anytime, through the establishment
of a public key infrastructure.
DIDs are **unique identifiers** (URIs) which are standardised by the [W3C](https://www.w3.org/TR/did-core/). They can
refer to any subject - from a person, to
an organization, to a thing or basically anything else for that matter. Before we have a look at how a DID is structured
and the benefit it provides, let's understand the shortcomings of current identifiers first and then see how the DID
solves those.
Video: https://www.youtube.com/watch?v=XajpI6yWxwA
## The shortcomings of current IDs
In the digital economy, data exchange happens a lot, which makes it increasingly important to be able to identify
persons, concepts or anything else for that matter in a secure and verifiable way.
A person today can have multiple different identifiers, like:
- name@surname.com
- https://www.surname.com
- 0000-0000-0000-0000 (ORCID based identifier, used to identify authors of scholarly communication)
All of these identifiers work, but none fulfills to be decentralized, persistent, resolvable and cryptographically
verifiable when put into the following questions.
#### Is the identifier decentralized?
- `https://www.surname.comdepends` on a single point of failure. What happens if the hosting side disappears?
- `0000-0000-0000-0000 (ORCID)` depends on the ORCID database. What happens if it is discontinued, hacked, etc?
#### Is the identifier persistent?
- `https://www.surname.com` If I do no longer pay for that domain, the identifier will be gone or even bought by
somebody else.
#### Is the identifier resolvable?
- How can I get additional information about `0000-0000-0000-0000 (ORCID)` identifiers?
#### Is the identifier verifiable?
- How can I prove that I own the domain, `https://www.surname.com`?
- If I stopped paying for my domain, `https://www.surname.com` and somebody else would buy it. How would somebody know
that
the information provided on the side was actually mine?
All those problems make it hard to be 100% sure when exchanging information, that the party we are exchanging
information with, is actually the party and not some malicious actor pretending to be the party.
## DID to the rescue
The design of DIDs, which is a new form of unique identifier that has been standardized by the W3C, can now help us
address these problems of current identifiers, by being:
**Decentralized**
- The DIDs no longer depend on centralized registries, identity providers, authorities, etc.
**Persistent**
- Once created, the did is permanently assigned to the subject.
**Resolvable**
- It is possible to find a basic set of information when resolving the did. Typically, this will lead to a DID Document.
**Cryptographically verifiable**
- There is a mechanism to cryptographically prove identity and ownership via information provided by the DID Document.
#### DID format:
The DID is a simple text string built up of three parts:

| Schema | The did URL schema identifier |
| ------------------------------ | --------------------------------------------------------- |
| DID Method | The identifier for the did method |
| DID Method-Specific Identifier | An identifier based on the requirements of the did method |
A variety of **"DID methods"**, which are different implementations of the DID specification, exist. Considering that
DID
methods differ in terms of how they are created, registered and resolved, different methods come with different
advantages and disadvantages.
For example, while DIDs are often anchored on Registries, such as EBSI (did:ebsi) or the Domain Name Service (did:web),
new methods emerged that do not require Registries because their distribution is based on peer-to-peer interactions (
e.g. did:key).
Now we can take any did, look at the method and resolve it based on the framework around the method. The resolved
content will most of the time be **JSON** or **JSON-LD**, although other data formats might also be added in the future.
The
resolved content is called **DID Document**.
## DID Document
A container of information holding:
**The Subject**
- The owner of the DID Document
**The Controllers**
- The entities allowed to introduce changes to the DID Document. The controller may or may not be identical to the "
subject". For example, if the DID Document belonged to a DID of a book. The controller would be the author or another
related person, rather than the book itself.
**Cryptographic data**
- The DID document contains the public keys of the corresponding entity. The keys might be of any algorithm (typically
elliptical curve keys or RSA keys are used), and are mostly encoded in the JWK format. However, other encoding formats
such as PEM or Multibase are also supported. The keys can be classified to be used in certain use cases such as:
authentication, verification, etc.
**Service endpoints**
- Services that the subject wants to mention
## DID General Flow

#### Example (did:ebsi)
The identifier `did:ebsi:2A9RkiYZJsBHT1nSB3HZAwYMNfgM7Psveyodxrr8KgFvGD5y` of the method `did:ebsi` would resolve to the
following DID Document:
```json
{
"@context": ["https://w3id.org/did/v1"],
// keys used for the authentication of the contoller
"authentication": ["did:ebsi:2A9RkiYZJsBHT1nSB3HZAwYMNfgM7Psveyodxrr8KgFvGD5y#1a7514b2d58141c3982021a6323b99bf"],
// the subject
"id": "did:ebsi:2A9RkiYZJsBHT1nSB3HZAwYMNfgM7Psveyodxrr8KgFvGD5y",
// the cryptographic data used for authentication, verification or other use cases
// utilizing public keys
"verificationMethod": [
{
"controller": "did:ebsi:2A9RkiYZJsBHT1nSB3HZAwYMNfgM7Psveyodxrr8KgFvGD5y",
"id": "did:ebsi:2A9RkiYZJsBHT1nSB3HZAwYMNfgM7Psveyodxrr8KgFvGD5y#1a7514b2d58141c3982021a6323b99bf",
"publicKeyJwk": {
"alg": "EdDSA",
"crv": "Ed25519",
"kid": "1a7514b2d58141c3982021a6323b99bf",
"kty": "OKP",
"use": "sig",
"x": "tqJADByHRU3YxswewQD4wQYXU9tB43j3PfjofsYEvqs"
},
"type": "Ed25519VerificationKey2018"
}
]
}
```
## Working With DIDs
walt.id offers a wide range of open-source tools that help you create, resolve and verify DIDs based on different
ecosystems easily. To get started, checkout:
- [DID Lib](https://github.com/walt-id/waltid-identity/tree/main/waltid-libraries/waltid-did) - work with DIDs in Kotlin/Java and JavaScript environments.
- [Issuer API](https://issuer.demo.walt.id/swagger/index.html#/Issuer%20onboarding/post_onboard_issuer) - create a set of keys and DIDs via the onboard functionality.
- [Wallet API](https://docs.walt.id/community-stack/wallet/did-management/overview.md) - create & manage DIDs for credential holders.
--------------------------------------------------------------------------------
PAGE: Digital Credentials
URL: https://docs.walt.id/concepts/digital-credentials
RAW: https://docs.walt.id/concepts/digital-credentials.md
STACK: Concepts (stack-agnostic, applies to both Community and Enterprise Stack)
--------------------------------------------------------------------------------
# Digital Credentials
Explore different credential formats below:
- [W3C Verifiable Credential](https://docs.walt.id/concepts/digital-credentials/verifiable-credentials-w3c.md)
- [SD-JWT VC (IETF)](https://docs.walt.id/concepts/digital-credentials/sd-jwt-vc.md)
- [mDL (ISO/IEC 18013-5)](https://docs.walt.id/concepts/digital-credentials/mdoc-mdl-iso.md)
--------------------------------------------------------------------------------
PAGE: Verifiable Credentials: A Simple Guide to How They Work
URL: https://docs.walt.id/concepts/digital-credentials/verifiable-credentials-w3c
RAW: https://docs.walt.id/concepts/digital-credentials/verifiable-credentials-w3c.md
STACK: Concepts (stack-agnostic, applies to both Community and Enterprise Stack)
--------------------------------------------------------------------------------
# What Are Verifiable Credentials? The Complete Guide
## The Problem with Digital Trust Today
In the modern digital ecosystem, identity is fragmented and insecure. The systems used to manage and verify who a person is online are largely built on outdated models that expose individuals and organizations to significant risk. Traditional digital identity relies on centralized and federated approaches, both of which present fundamental flaws.
In a **centralized identity** model, each service a person uses—from social media to online banking—maintains its own separate database of user credentials. This approach creates countless data silos, each a potential target for attack. A breach at any single service provider can expose the sensitive personal information of millions of users.
The **federated identity** model, exemplified by "Sign in with Google" or "Log in with Facebook," was developed to simplify this process. While more convenient, it introduces a different set of problems. It concentrates immense power in the hands of a few large identity providers, who can track user activity across different websites and services. This creates significant privacy concerns, as personal data can be correlated and used in ways the individual never intended or explicitly consented to. Both models suffer from a core weakness: the user is never truly in control of their own data.
### A Comparison of Digital Identity Models
## Table 1: A Comparison of Digital Identity Models
| Category | Centralized Identity | Federated Identity | Self-Sovereign Identity (SSI) |
|---|---|---|---|
| Data Control | Controlled by a single service provider (e.g., a website). | Controlled by a third-party Identity Provider (e.g., Google, Facebook). | Controlled by the individual user. |
| User Privacy | Low, as the provider controls data collection and use. | Medium. Data is shared between services, creating potential for tracking. | High. User shares only necessary data ([selective disclosure](https://docs.walt.id/concepts/selective-disclosure.md)). |
| Single Point of Failure | High. A breach at the provider compromises all user data. | High. A breach at the Identity Provider compromises access to many services. | Low. Decentralized nature eliminates single targets for large-scale breaches. |
| Portability | Low. Identity is locked to a single service. | Medium. Identity is portable across services that support the provider. | High. Credentials are held by the user and are interoperable by design. |
To solve these deep-seated issues of security, privacy, and control, a new technical paradigm is required. This is where Verifiable Credentials (VCs) emerge as a foundational technology for a more secure and user-centric internet. In essence, Verifiable Credentials are tamper-evident, cryptographically secure digital versions of the credentials we use every day, giving individuals control over their own data and allowing them to share proof of their achievements and attributes without revealing unnecessary personal information.
## Verifiable Credentials: The Digital Wallet for Your Achievements
The easiest way to understand Verifiable Credentials is to compare them to a physical wallet. A physical wallet holds various items that prove things about a person: a driver's license proves the ability to drive, a university ID proves student status, and an insurance card proves coverage. Each card was issued by a different trusted authority (the DMV, a university, an insurance company) and now resides in the individual's possession, under their direct control.
A **digital wallet** for **Verifiable Credentials** functions in precisely the same way, but for the digital world. It is a secure application, typically on a smartphone, where an individual can store and manage their digital credentials. Just as a person decides whether to pull out their driver's license or just state their name, the holder of a digital wallet has complete control over when, where, and with whom they share their digital credentials.
This analogy reveals a crucial shift in thinking away from a single, monolithic "digital identity." A wallet does not contain a person's "identity"; it holds a collection of distinct, verifiable attributes or claims. One credential makes a claim about driving privileges, another about educational attainment. The power of VCs lies in this granularity. They are not about creating one universal online ID but about empowering users to manage a collection of discrete, context-specific claims about themselves. "Verifying credentials," in this context, is the process where a third party can request claims about you and then cryptographically check the authenticity of these requested claims instantly, without needing to contact the original issuer every time.
## The Core Components: The Triangle of Trust
The **Verifiable Credentials** ecosystem is built on the interaction between three core roles. This relationship is often referred to as the "triangle of trust" and is essential for the system's function.
### The Issuer: The Source of Truth
The **Issuer** is an organization that attests to a claim about a subject and creates a cryptographically signed credential containing that claim.
- **Examples**: A government agency issuing a digital passport, a university issuing a digital diploma, a bank issuing proof of income, or an employer issuing an employee badge.
### The Holder: The User in Control
The **Holder** is the individual or entity that receives the credential from the **Issuer**. The Holder stores the credential in their personal digital wallet and has sole authority over how and when it is shared.
### The Verifier: The Trusting Party
The **Verifier** is the person or organization that needs to confirm a claim about the **Holder**. The Verifier requests proof from the Holder, who then presents their **Verifiable Credential**. The Verifier can then check the credential's authenticity and integrity instantly.
- **Examples**: An employer verifying a job applicant's degree, a website confirming a user is over 18, a border agent checking a digital passport, or a hospital confirming a doctor's medical license.
## How Verifiable Credentials Work: A Step-by-Step Process
The lifecycle of a **Verifiable Credential** follows a clear four-step process:
### Step 1: Issuance (Creating the Credential)
The process begins when an **Issuer** validates information about a potential **Holder** through a standard process, such as a Know Your Customer (KYC) check or student enrollment confirmation. Once the information is confirmed, the **Issuer** generates a digital credential containing specific claims (e.g., "Date of Birth: 1990-05-15"). This credential is then cryptographically signed with the **Issuer**'s private key, creating a tamper-evident seal, and is securely transmitted to the **Holder**.
### Step 2: Storage (The Holder's Digital Wallet)
The **Holder** receives the credential and accepts it into their digital wallet. This wallet can be an app or web application on their phone or laptop. Once stored, the credential resides entirely under the **Holder**'s control, independent of the **Issuer**'s systems. This is a critical distinction from traditional models where data remains on a company's servers.
### Step 3: Presentation (Sharing Proof, Not Data)
When a **Verifier** needs to confirm a claim, they request it from the **Holder**. The **Holder** uses their wallet to generate a **Verifiable Presentation**. This is a bundle that contains the relevant credential (or parts of it) and is also cryptographically signed by the **Holder** using their private key. This second signature proves to the **Verifier** that the **Holder** is the rightful owner of the credential and consents to sharing it.
Crucially, this step enables selective disclosure. The Holder can choose to reveal only the specific claims necessary for a given transaction. For example, to prove they are of legal drinking age, they can present only the claim "is over 21" without revealing their name, address, or exact date of birth, dramatically enhancing privacy.
### Step 4: Verification (The Cryptographic Handshake)
The **Verifier** receives the **Verifiable Presentation** and performs a series of automated cryptographic checks. First, it verifies the **Holder**'s signature to confirm ownership. Second, it verifies the **Issuer**'s original signature on the credential. To do this, the **Verifier** retrieves the **Issuer**'s public key from a trusted data registry. If both signatures are valid, the **Verifier** can be certain that the credential is authentic, was issued by the claimed **Issuer**, has not been altered, and is being presented by its rightful owner. This entire verification happens in seconds, without the **Verifier** ever needing to contact the **Issuer** directly.
## What Are Examples of Verifiable Information?
The applications for **Verifiable Credentials** span nearly every industry, transforming processes that currently rely on slow, insecure, and paper-based verification.
### Example 1: The University Diploma
A university (**Issuer**) issues a digital diploma as a VC to a graduating student (**Holder**). When applying for a job, the student can instantly present this credential to a potential employer (**Verifier**). The employer's system can cryptographically verify its authenticity in seconds. This eliminates the need for expensive and time-consuming manual background checks and calls to the university registrar, streamlining the hiring process for both parties.
### Example 2: The Proof of Residence
A Public Utility Company (**Issuer**) issues a monthly digital bill as a Verifiable Credential (VC) to a resident (**Holder**). When signing up for a new internet service, the **Holder** needs to prove they live at the service address. The **Holder** can present a Verifiable Presentation to the Internet Service Provider (**Verifier**) that proves only two claims: "Full Name" and "Residential Address." The ISP's system can cryptographically confirm the validity of the address and that it is associated with the **Holder**'s name, without ever seeing the **Holder**'s utility account number, energy consumption, or other information. This fulfills the ISP's business needs while protecting the resident's other data.
### Example 3: The Professional License
A medical board (**Issuer**) issues a license-to-practice VC to a qualified doctor (**Holder**). When the doctor joins a new hospital, the hospital's administration (**Verifier**) can instantly confirm that the doctor's license is authentic and has not been revoked. This ensures patient safety, satisfies regulatory compliance, and dramatically speeds up the onboarding and credentialing process for medical professionals.
## DIDs vs. Verifiable Credentials: What's the Difference?
Within the world of **Self-Sovereign Identity (SSI)**, two foundational technologies work together: **Decentralized Identifiers ([DIDs](https://docs.walt.id/concepts/decentralised-identifiers.md))** and **Verifiable Credentials (VCs)**. They are distinct but complementary.
A simple analogy helps clarify their roles: a DID is like your physical street address. It is a unique, persistent identifier for your home. Knowing the address `123 Main Street` doesn't tell someone anything personal about who lives there, their credit score, or their profession. It's simply a stable endpoint where information can be sent and received. A VC is like an Official Letter delivered to that mailbox, such as a diploma or a utility bill. Each letter is a specific claim from a specific issuer (a university, a power company).
More technically, **DID**s are a new type of identifier that individuals can create for themselves, which they own and control completely. A DID can be the anchor of a digital identity (some identity ecosystems only use private and public keys without DIDs). It contains no personal data itself; instead, it resolves to a public "DID Document" that contains the cryptographic public keys and potentially service endpoints needed to interact with the **DID**'s owner securely.
**Verifiable Credentials** are the statements or claims that are cryptographically bound to a **DID**. The **Issuer** signs a credential and addresses it to the **Holder**'s **DID**. From a technical standpoint, this relationship is the critical component that unlocks true data mobility and independence from any single issuer. Because the credential is tied to the user-controlled **DID** rather than an account on the **Issuer**'s system, the **Holder** can truly own and manage it for life.
## Understanding the W3C Verifiable Credentials Data Model
**Verifiable Credentials (VCs)** are based on open standards from the World Wide Web Consortium (W3C), ensuring interoperability and avoiding vendor lock-in. The **W3C Verifiable Credentials Data Model (VCDM)** defines a standard way to express credentials (the data model) and leaves room for multiple cryptographic encodings to make them tamper-evident and verifiable. Most commonly, VCs are secured using Data Integrity proofs (JSON-LD), JOSE/JWT (JWS), or [SD-JWT](https://docs.walt.id/concepts/digital-credentials/sd-jwt-vc.md) (selective disclosure with JWT). The last one enables users to share only a subset of the claims in a credential.
### A quick note on VC Data Model versions
W3C has published two versions of the Verifiable Credentials Data Model: v1.1 (W3C Recommendation on March 3, 2022) and v2.0 (W3C Recommendation on May 15, 2025). Both define the same fundamental concepts (issuer, credentialSubject/claims, and a verifiable signature), while v2.0 refines terminology and alignment with modern security mechanisms (e.g., JOSE/COSE and Data Integrity) and improves extensibility. Many ecosystems still run v1.1, with adoption of v2.0 growing.
### The three core parts of a VC
### Claims (the pieces of information)
The `credentialSubject` block contains the claims about the subject (**Holder**). For a university diploma, claims could include degree type, major, and graduation date.
### Metadata (Who issued it and when)
Includes information like the issuer’s `DID`, the `issuanceDate`, and a unique credential `id`. This helps a verifier understand origin and lifecycle.
### Cryptographic proof or signature (the tamper-proof seal)
The cryptographic proof or signature is what makes the credential verifiable. Depending on the chosen approach, you’ll see either a proof block (Data Integrity / JSON-LD) or a JWT/JWS/SD-JWT signature that covers the credential payload. W3C’s VC-JOSE-COSE spec defines how to secure VCDM payloads with JOSE (JWT), COSE (CBOR), and SD-JWT.
### Example 1 — Data Integrity (JSON-LD) VC
```json
{
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://www.w3.org/2018/credentials/examples/v1"
],
"id": "http://example.edu/credentials/3732",
"type": ["VerifiableCredential", "UniversityDegreeCredential"],
"issuer": "did:example:76e12ec712ebc6f1c221ebFEB1f",
"issuanceDate": "2024-01-01T19:23:24Z",
"credentialSubject": {
"id": "did:example:ebfeb1f712ebc6f1c276e12ec21",
"degree": {
"type": "Bachelor of Science",
"name": "Computer Science"
}
},
"proof": {
"type": "Ed25519Signature2018",
"created": "2024-01-01T19:23:24Z",
"proofPurpose": "assertionMethod",
"verificationMethod": "https://example.edu/issuers/14#keys-1",
"jws": "eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..l9d_264a..."
}
}
```
Here, the VCDM payload is secured with a **Data Integrity** proof (a JSON-LD proof block).
### Example 2 — VC-JWT (JOSE/JWS) representation (simplified)
In a JWT representation defined by VC-JOSE-COSE, the same VCDM fields appear inside the JWT payload (e.g., under `vc`), and the credential is signed with JWS. Instead of a JSON-LD proof block, verification uses the JWT header+signature.
```json
// (header)
{ "alg": "EdDSA", "typ": "vc+jwt" }
// (payload) — key VCDM fields embedded as JSON
{
"iss": "did:example:76e12ec712ebc6f1c221ebFEB1f",
"nbf": 1704137004,
"vc": {
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://www.w3.org/2018/credentials/examples/v1"
],
"type": ["VerifiableCredential", "UniversityDegreeCredential"],
"id": "http://example.edu/credentials/3732",
"issuanceDate": "2024-01-01T19:23:24Z",
"issuer": "did:example:76e12ec712ebc6f1c221ebFEB1f",
"credentialSubject": {
"id": "did:example:ebfeb1f712ebc6f1c276e12ec21",
"degree": { "type": "Bachelor of Science", "name": "Computer Science" }
}
}
}
// The compact JWS string is what gets transported/presented.
```
## The Future is Verifiable: Key Benefits
The shift toward **Verifiable Credentials** and **Self-Sovereign Identity** offers great benefits for individuals, organizations, and the digital economy as a whole.
### Enhanced Security & Fraud Prevention
Because VCs are **cryptographically** signed and tamper-evident, they are exponentially more difficult to forge than physical documents or simple digital files. This provides strong guarantees against fraud. Furthermore, by moving data from centralized, high-value targets to individual **digital wallets**, the SSI model drastically reduces the risk and impact of large-scale data breaches.
### Better User Privacy & Control
Better user privacy and control is perhaps the most significant benefit. Through **selective disclosure**, users can prove a specific claim without over-sharing personal data. This principle of data minimization is a cornerstone of modern privacy regulations and is built into VCs.
### Interoperability and Efficiency
Built on open W3C standards, VCs are designed to work across different services, industries, and international borders, creating a digital ID model that works globally. This interoperability eliminates redundant and inefficient verification processes. Instead of every new service conducting its own costly identity check, it can simply verify a credential that has already been issued by a trusted source. This speeds up everything from customer onboarding and employee credentialing to loan applications and supply chain management.
## Become a Verifiable Credential Issuer, Verifier, or Wallet Provider with walt.id
Learn how to issue, verify, and store W3C VCs quickly and easily. Choose the free open-source Community Stack (Apache-2.0) or the production-ready Enterprise Stack from walt.id, trusted by :traction-devs{} developers and organizations worldwide.
### Product Editions
- [The Community Stack](https://docs.walt.id/community-stack/home/the-community-stack.md) (Apache-2.0) — trusted open source multi-platform libs, powerful APIs and easy-to-use white label apps for digital identity and wallets.
- [The Enterprise Stack](https://docs.walt.id/enterprise-stack/home/the-enterprise-stack.md) — a solution for building secure, compliant and robust identity and wallet solutions or platforms at scale.
### Guides
- [How to issue a W3C Verifiable Credential](https://docs.walt.id/community-stack/issuer/credential-issuance/vc-oid4vc.md)
- [How to verify a W3C Verifiable Credential](https://docs.walt.id/community-stack/verifier/credential-verification/vc-oid4vc.md)
- [How to Store a W3C Verifiable Credential](https://docs.walt.id/community-stack/wallet/credential-exchange/guides/accept-w3c-vc-oid4vci.md)
- [How to Present a W3C Verifiable Credential](https://docs.walt.id/community-stack/wallet/credential-exchange/guides/present-digital-credentials-oid4vp.md)
--------------------------------------------------------------------------------
PAGE: SD-JWT VC Explained: A Practical Guide for 2026
URL: https://docs.walt.id/concepts/digital-credentials/sd-jwt-vc
RAW: https://docs.walt.id/concepts/digital-credentials/sd-jwt-vc.md
STACK: Concepts (stack-agnostic, applies to both Community and Enterprise Stack)
--------------------------------------------------------------------------------
# SD-JWT VC: The Ultimate Guide to Verifiable Credentials with Selective Disclosure
A Selective Disclosure JSON Web Token Verifiable Credential (SD-JWT VC) is a new type of digital credential format, based on Internet Engineering Task Force (IETF) standards, designed for privacy-preserving information sharing.
In this guide we learn how SD-JWT VCs work, look at runnable code examples and do a deep dive into more advanced topics like revocation and type metadata that are often buried in dense specifications.
## What is an SD-JWT VC? A Plain English Introduction
To understand the importance of SD-JWT VC, it is essential to first grasp the problem it is designed to solve.
### The Privacy Problem: Why Sharing Your Whole ID is a Bad Idea
Traditional identity documents, from a physical driver's license to standard digital certificates, operate on an "all-or-nothing" principle. To prove a single fact—for instance, that a person is over 21 years of age—an individual is often forced to reveal a full set of personal data, including their name, home address, and exact date of birth. This over-sharing of information is a significant privacy risk.
### The Solution: How SD-JWTs Allow You to Share Only What's Necessary
The SD-JWT VC provides an elegant solution to this long-standing privacy dilemma. It is a verifiable credential format that enables selective disclosure, a mechanism that gives individuals granular control to reveal only specific pieces of information (claims) while keeping the rest private and cryptographically hidden.
Selective disclosure is achieved through a clever architectural design. Instead of including all personal data directly in the credential, an SD-JWT VC only contains cryptographic “placeholders” (hashes) for sensitive claims. The actual values, called **Disclosures**, are stored separately. When a user (**Holder**) needs to prove something, they share the main token plus only the specific **Disclosures** the **Verifier** requires. The **Verifier** then checks that these **Disclosures** match the hashes in the token, ensuring they are genuine and were issued by the original **Issuer**.
This shift from a monolithic data-sharing model to an "opt-in per claim" model is profound. It fundamentally alters the balance of power, moving control from the verifier (who previously received everything) to the holder (who now curates the flow of each individual attribute). Which is why SD-JWT VCs are a mandatory standard for **Issuers**, **Verifiers** and **Wallets** in regulations such as [eIDAS2](https://walt.id/eidas2) in the EU.
## Core Concepts: The Building Blocks of an SD-JWT VC
An SD-JWT VC presentation is composed of up to three distinct components that work together.
### The SD-JWT: The Main Credential with Hidden Claims (Hashes)
The core of the credential is the SD-JWT itself, which is a standard Issuer-signed JWT. Its structure, however, is unique. Instead of a payload containing all user claims in plaintext, it holds a mixture of two types of information:
1. Claims that are always visible and are not considered sensitive.
2. A special `_sd` (selective disclosure) JSON array that contains digests—cryptographic hashes (e.g., SHA-256) of the claims that are meant to be selectively disclosable.
The issuer's digital signature covers the entire JWT payload, including the plaintext claims and the `_sd` array of hashes. This signature provides a guarantee of integrity; it ensures that the set of cryptographic commitments cannot be altered, and no claims can be added or removed after the credential has been issued. The SD-JWT serves as the immutable root of trust for the entire credential.
### The Disclosures: The Individual Pieces of Information
**Disclosures** are the individual, plaintext pieces of information that correspond to the hashes stored in the SD-JWT's `_sd` array. They are the actual data attributes that the holder can choose to reveal. Each **Disclosure** is a separate, Base64URL-encoded string that, when decoded, represents a JSON array with three specific elements:
1. A unique, randomly generated salt to prevent pre-computation attacks (e.g., rainbow tables).
2. The claim name (e.g., "family_name").
3. The claim value (e.g., "Doe").
During the issuance process, the issuer generates these **Disclosures**, hashes them to create the digests for the SD-JWT, and then provides both the signed SD-JWT and the full set of plaintext **Disclosures** to the **Holder**'s digital wallet. The **Disclosures** are never part of the signed JWT itself but are unavoidably linked to it through their corresponding hashes.
### The Holder Binding JWT: Proving You Own the Credential
A standard JWT is a "bearer token," meaning anyone who possesses the token can use it. This presents a significant security risk if the token is stolen.
**Holder Binding**, also known as **Key Binding**, is a critical mechanism within the SD-JWT VC framework that mitigates this risk by cryptographically tying the credential to its legitimate owner.
Holder Binding is achieved by having the issuer embed the holder's public key (or a reference to it) within the SD-JWT, typically in a cnf (confirmation) claim. When the holder needs to present the credential, the verifier provides a unique challenge, such as a random string called a "nonce." The holder must then use their corresponding private key to sign a new, short-lived token containing this nonce. This new token is the Holder Binding JWT. The verifier can then use the public key from the main SD-JWT to validate the signature on the **Holder Binding JWT**. A successful validation proves that the person presenting the credential is in possession of the correct private key and is, therefore, the legitimate owner.
These three components—the **SD-JWT**, the **Disclosures**, and the **Holder Binding JWT**—represent a clear separation of concerns that mirrors the roles in the classic identity triangle. The SD-JWT is the **Issuer**'s attestation, a signed, tamper-evident statement of facts. The **Disclosures** represent the **Holder**'s data, a collection of granular attributes under their direct control. The **Holder Binding JWT** proves that the Holder controls the credential and agrees to its use, created only for the specific moment of presentation.
## How It Works: The 3-Step Flow from Issuance to Verification
The lifecycle of an SD-JWT VC involves a coordinated interaction between three parties: the **Issuer**, the **Holder**, and the **Verifier**. This flow is often governed by standardized protocols like [**OID4VCI**](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md) and [**OID4VP**](https://docs.walt.id/concepts/data-exchange-protocols/openid4vp.md) to ensure interoperability.
### Step 1: An Issuer Creates the Credential for the Holder
The process begins when an entity, such as a university, government agency, or employer, wants to issue a credential to an individual.
1. The Issuer collects the Holder's claims (e.g., name, date of birth, degree earned).
2. The Issuer determines which of these claims should be selectively disclosable based on their sensitivity.
3. For each disclosable claim, the Issuer generates a unique salt, constructs a Disclosure array in the format [salt, claim_name, claim_value], and calculates its cryptographic hash (e.g., using SHA-256).
4. The Issuer then assembles the JWT payload. This payload includes any non-disclosable claims in plaintext, along with the calculated hashes of the disclosable claims, which are placed in the _sd array.
5. The Issuer digitally signs this entire JWT payload using its private key, creating the final SD-JWT.
6. Finally, the Issuer securely transmits both the signed SD-JWT and the full set of corresponding plaintext Disclosures to the Holder's digital wallet, often using the official [OID4VCI exchange protocol](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md).
### Step 2: The Holder Selects Disclosures to Present
At a later time, the Holder needs to prove a piece of information to a third party.
1. A Verifier (e.g., a website, a merchant) requests specific information from the Holder, such as proof of age or academic qualifications. This request is often formatted according to the [OID4VP protocol](https://docs.walt.id/concepts/data-exchange-protocols/openid4vp.md).
2. The Holder's wallet, acting on their behalf, identifies the required Disclosures from its secure storage to satisfy the Verifier's request. For an age check, it might select only the "is_over_21" Disclosure, leaving all other information private.
3. The wallet combines the original SD-JWT with only the selected Disclosures.
4. If the Verifier requires proof of ownership (which is best practice), the wallet also prompts the Holder to create and sign the Holder Binding JWT using their private key.
5. This combined package—containing the SD-JWT, the selected Disclosures, and optionally the Holder Binding JWT—is known as a Verifiable Presentation. This presentation is then sent to the Verifier.
### Step 3: A Verifier Checks the Presented Information
Upon receiving the Verifiable Presentation, the Verifier performs a series of checks to validate its authenticity and the claims within it.
1. The Verifier first checks the Issuer's signature on the main SD-JWT. This confirms that the credential is authentic and has not been tampered with since issuance. This step typically involves retrieving the Issuer's public key, which may be done via a URL in the token or from a trusted registry.
2. Next, for each Disclosure it received, the Verifier independently re-calculates the hash using the exact same method as the Issuer (e.g., hashing the Base64URL representation of the [salt, claim_name, claim_value] array).
3. The Verifier then checks if these newly calculated hashes are present in the _sd array within the SD-JWT's payload. A match confirms that the disclosed claim is legitimate and was part of the original set of claims signed by the Issuer.
4. Finally, if a Holder Binding JWT was included, the Verifier uses the public key from the SD-JWT's cnf claim to verify its signature. This final step confirms that the person presenting the credential is its rightful owner, preventing the use of stolen credentials.
## The Key Comparison: SD-JWT VC vs. mDoc/mDL
The digital identity landscape is not monolithic, and SD-JWT VC is one of several important emerging standards. Its most notable counterpart is [mDoc](https://docs.walt.id/concepts/digital-credentials/mdoc-mdl-iso.md) (Mobile Document), the format specified by ISO/IEC for mDL (Mobile Driving Licenses).
### Key Differences in Format and Structure
The fundamental differences between SD-JWT VC and mDoc stem from their origins and intended applications.
- **Data Format**: SD-JWT VC is built upon JSON and JWTs. These are text-based, web-native technologies that are deeply familiar to the vast majority of software developers, making integration into web services and APIs straightforward. In contrast, mDoc is based on CBOR (Concise Binary Object Representation), a binary data format designed for efficiency, resulting in smaller message sizes that are well-suited for constrained environments like device-to-device communication.
- **Standardization Body**: The development of SD-JWT VC is led by the IETF (Internet Engineering Task Force), the body responsible for many core internet protocols. SD-JWT VC's evolution is driven by the web and open identity communities. mDoc/mDL, on the other hand, is an ISO/IEC standard (specifically ISO/IEC 18013-5), which was standardized by ISO/IEC (JTC 1/SC 17) with significant input from issuing authorities and government stakeholders.
### Use Cases: When to Choose One Over the Other
The choice between SD-JWT VC and mDoc depends heavily on the specific interaction model.
- **Choose SD-JWT VC for**: Web-based authentication flows, online identity verification for services like banking or e-commerce, and any scenario where developer familiarity with the ubiquitous JSON/JWT ecosystem is a key consideration. Its text-based format is optimized for standard web protocols.
- **Choose mDoc/mDL for**: High-assurance, in-person interactions, particularly those that may require offline verification capabilities using transport protocols like NFC, Bluetooth, or QR codes. It is the mandated or preferred format for government-issued mobile identity documents, such as the mDL, in many jurisdictions.
The mandated support for both formats within comprehensive frameworks like Europe's eIDAS 2.0 is not a sign of indecision but a strategic acknowledgment that a single credential format cannot efficiently serve the entire spectrum of digital identity needs. The modern identity ecosystem requires both a solution optimized for the online world and one for the physical world. This implies that the future is multi-format, placing a significant responsibility on the digital wallet, which must act as a universal translator and protocol manager, abstracting this complexity away from the end-user and ensuring seamless interoperability across different contexts.
Ultimately, the final deciding factor comes down to the use case and jurisdiction under which the credential should be deployed.
### A Quick-Reference Comparison Table
The following table summarizes the key distinctions between the two credential formats.
| Feature | SD-JWT VC | mDoc/mDL |
|----------------------------|--------------------------------------------------------|-----------------------------------------------------------------|
| **Standardization Body** | IETF (Internet Engineering Task Force) | ISO/IEC (International Organization for Standardization) |
| **Data Format** | JSON (Text-based) | CBOR (Binary) |
| **Primary Use Case**| Online / Remote Verification | In-Person / Device-to-Device Verification |
| **Common Transport Protocols** | HTTP-based (e.g., OID4VP) | NFC, Bluetooth, Wi-Fi Aware, QR Codes |
| **Developer Ecosystem** | Familiar to web developers (JWT, JSON) | Requires familiarity with binary data structures |
| **Example Application** | Verifying identity for a web service | Presenting a mobile driver's license to law enforcement |
## A Practical Example: Create & Verify an SD-JWT VC via API
To move from theory to practice, we will do a complete end-to-end flow using the open-source APIs of walt.id. First, we will create a OID4VCI credential offer for an SD-JWT VC. Second, we will receive the credential in the walt.id web-wallet. Third, we will request and verify the SD-JWT VC
using the walt.id Verifier API via OID4VP.
All the API endpoints we will use come from the walt.id Community Stack Issuer, Verifier and Wallet product.
### 1. Create an OID4VCI Offer for an SD-JWT VC
To create the OID4VCI credential offer, we will use the `/issue` endpoint of the walt.id Issuer API. We will be issuing an identity credential
in the SD-JWT VC format and will mark the `birthdate` attribute as selectively disclosable.
Use the following command from your terminal to create the offer:
```
curl -X 'POST' \
'https://issuer.demo.walt.id/openid4vc/sdjwt/issue' \
-H 'accept: text/plain' \
-H 'statusCallbackUri: https://example.com/$id' \
-H 'Content-Type: application/json' \
-d '{
"issuerKey": {
"type": "jwk",
"jwk": {
"kty": "OKP",
"d": "mDhpwaH6JYSrD2Bq7Cs-pzmsjlLj4EOhxyI-9DM1mFI",
"crv": "Ed25519",
"kid": "Vzx7l5fh56F3Pf9aR3DECU5BwfrY6ZJe05aiWYWzan8",
"x": "T3T4-u1Xz3vAV2JwPNxWfs4pik_JLiArz_WTCvrCFUM"
}
},
"issuerDid": "did:key:z6MkjoRhq1jSNJdLiruSXrFFxagqrztZaXHqHGUTKJbcNywp",
"credentialConfigurationId": "identity_credential_vc+sd-jwt",
"credentialData": {
"given_name": "John",
"family_name": "Doe",
"email": "johndoe@example.com",
"phone_number": "+1-202-555-0101",
"address": {
"street_address": "123 Main St",
"locality": "Anytown",
"region": "Anystate",
"country": "US"
},
"birthdate": "1940-01-01",
"is_over_18": true,
"is_over_21": true,
"is_over_65": true
},
"mapping": {
"id": "",
"iat": "",
"nbf": "",
"exp": ""
},
"authenticationMethod": "PRE_AUTHORIZED",
"selectiveDisclosure": {
"fields": {
"birthdate": {
"sd": true
}
},
"decoyMode": "NONE",
"decoys": 0
}
}'
```
Your response should look like the following:
```
openid-credential-offer://issuer.potential.walt-test.cloud/?credential_offer_uri=https%3A%2F%2Fissuer.potential.walt-test.cloud%2Fopenid4vc%2FcredentialOffer%3Fid%3D9aabdb65-defe-464b-baa0-9cc13b36074a
```
### 2. Receive the SD-JWT VC in the Wallet
We can now take the credential offer from the previous step and paste it into the input field after clicking the "Receive" button in the walt.id Web-Wallet. Click [here](https://wallet.demo.walt.id/) to go to the wallet.
### 3. Verify the Credential via OID4VP
To request and verify the SD-JWT VC we just received via OID4VP, we will use the
`/verify` endpoint of the walt.id Verifier API.
Use the following command from your terminal to create the OID4VP credential
request.
```
curl -X POST 'https://verifier.demo.walt.id/openid4vc/verify' \
-H 'accept: */*' \
-H 'authorizeBaseUrl: openid4vp://authorize' \
-H 'responseMode: direct_post' \
-H 'successRedirectUri: https://example.com/success?id=$id' \
-H 'errorRedirectUri: https://example.com/error?id=$id' \
-H 'Content-Type: application/json' \
-d '{
"request_credentials": [
{ "vct": "identity_credential", "format": "dc+sd-jwt" }
]
}'
```
Your response should look like the following:
```
openid4vp://authorize?response_type=vp_token&client_id=https%3A%2F%2Fverifier.demo.walt.id%2Fopenid4vc%2Fverify&response_mode=direct_post&state=sHti6w6UMPgX&presentation_definition_uri=https%3A%2F%2Fverifier.demo.walt.id%2Fopenid4vc%2Fpd%2FsHti6w6UMPgX&client_id_scheme=redirect_uri&client_metadata=%7B%22authorization_encrypted_response_alg%22%3A%22ECDH-ES%22%2C%22authorization_encrypted_response_enc%22%3A%22A256GCM%22%7D&nonce=3d8605c5-348f-466c-b6fa-f87371ffed88&response_uri=https%3A%2F%2Fverifier.demo.walt.id%2Fopenid4vc%2Fverify%2FsHti6w6UMPg
```
### 4. Present the Credential
Back in [the wallet](https://wallet.demo.walt.id/), you can press the "Present"
button and paste what you got back in the previous call in the input field. If
you get redirected to https://example.com/success you know the verification was a success.
**Note:**
Note: We configured `example.com/success` as the success redirect URI when we
created the OID4VP credential request with the verifier API.
If you want to check out the detailed verification response, you can check out the guide [here](https://docs.walt.id/community-stack/verifier/credential-verification/sd-jwt-vc-oid4vc.md#retrieving-the-verification-status).
## Advanced Topics You Need to Know
To fully grasp the capabilities and nuances of SD-JWT VC, it is important to understand several advanced concepts that are critical for real-world implementations.
### Understanding Credential Type Metadata
An important feature for ensuring interoperability in a decentralized ecosystem is the ability to understand the structure and semantics of a credential. SD-JWT VC addresses this through the `vct` (Verifiable Credential Type) claim.
The `vct` claim, defined and commonly required by profiles, is a non-disclosable string within the JWT payload that acts as a unique identifier or schema for the credential. Its value can be a URL that points to a Type Metadata document. This external JSON document provides rich information about the credential type, including:
- Human-readable names and descriptions for display purposes.
- Display and rendering information, allowing wallets to present credentials from different issuers in a consistent and user-friendly way.
- Most importantly, a formal JSON Schema that defines the expected claims, their data types, and structural rules.
This metadata mechanism is powerful. It enables wallets to render unfamiliar credentials correctly and allows verifiers to perform automated validation against a known schema, ensuring that a received credential conforms to the expected format before processing its contents.
**Note:**
**Using Your Own VCT URLs**: Issuers can configure their own VCT URLs, enabling the use of shared VCTs in ecosystem setups where multiple issuers reference the same schema repository. This is particularly useful for organizations that require standardized schemas across their ecosystem. The credential data is validated against the schema defined in the VCT during issuance. Learn more about configuring custom VCT URLs in the [Enterprise Stack](https://docs.walt.id/enterprise-stack/services/issuer-service/configurations/general-config.md) or [Community Stack](https://docs.walt.id/community-stack/issuer/configurations/config-files/credential-issuer-metadata.md) documentation.
### The Challenge of SD-JWT VC Revocation Strategies
A credential that is valid upon issuance may need to be revoked later (e.g., if an employee leaves a company or a license is suspended). An effective revocation mechanism is therefore a critical piece of identity infrastructure. SD-JWT VC provides a modern, privacy-preserving solution for this challenge through an optional `status` claim in the JWT payload.
The primary mechanism specified is the use of **Status Lists**. The status claim within a credential can contain information that points a verifier to a **Status List Token**, which is itself a JWT issued by a trusted authority (e.g. the issuer). This token contains a highly compressed bitstring (a list of 0s and 1s). Each position (or index) in the bitstring corresponds to a specific credential, and the value of the bit indicates its status (e.g., `0` for valid, `1` for revoked).
This approach is highly efficient and privacy-enhancing. To check if a credential has been revoked, a verifier only needs to fetch the latest Status List and check the value of a single bit at a specific index. The verifier does not need to reveal to the status authority which specific credential it is checking, thus preserving the privacy of the interaction. This is a significant improvement over older revocation methods like Certificate Revocation Lists (CRLs), which were often large and slow to process.
## Become an SD-JWT VC IETF Issuer, Verifier, or Wallet Provider with walt.id
Learn how to issue, verify, and store SD-JWT VCs quickly and easily. Choose the free open-source Community Stack (Apache-2.0) or the production-ready Enterprise Stack from walt.id, trusted by :traction-devs{} developers and organizations worldwide.
### Product Editions
- [The Community Stack](https://docs.walt.id/community-stack/home/the-community-stack.md) (Apache-2.0) — trusted open source multi-platform libs, powerful APIs and easy-to-use white label apps for digital identity and wallets.
- [The Enterprise Stack](https://docs.walt.id/enterprise-stack/home/the-enterprise-stack.md) — a solution for building secure, compliant and robust identity and wallet solutions or platforms at scale.
### Guides
- [How to issue an SD-JWT VC](https://docs.walt.id/community-stack/issuer/credential-issuance/sd-jwt-vc-oid4vc.md)
- [How to verify an SD-JWT VC](https://docs.walt.id/community-stack/verifier/credential-verification/sd-jwt-vc-oid4vc.md)
- [How to Store an SD-JWT VC](https://docs.walt.id/community-stack/wallet/credential-exchange/guides/accept-w3c-vc-oid4vci.md)
- [How to Present an SD-JWT VC](https://docs.walt.id/community-stack/wallet/credential-exchange/guides/present-digital-credentials-oid4vp.md)
--------------------------------------------------------------------------------
PAGE: What Is a Mobile Driver's License (mDL)? Your 2026 Guide
URL: https://docs.walt.id/concepts/digital-credentials/mdoc-mdl-iso
RAW: https://docs.walt.id/concepts/digital-credentials/mdoc-mdl-iso.md
STACK: Concepts (stack-agnostic, applies to both Community and Enterprise Stack)
--------------------------------------------------------------------------------
# What Is a Mobile Driver's License (mDL)? Your 2026 Guide
The mobile driver's license (mDL) is here, and it represents one of the most significant shifts in personal identity since the creation of the photo ID. Stored securely in your smartphone's digital wallet, an mDL is far more than just a picture of your plastic license; it is a live, cryptographically secure digital credential issued and managed by a trusted issuer, e.g. a government authority. This technology promises a future of greater convenience, enhanced security, and greater personal privacy. In this guide, you will learn exactly what an mDL is, how the underlying technology works, its key benefits, where it's available globally, and a clear-eyed look at the critical debate surrounding its security and privacy implications.
## What is a Mobile Driver's License (mDL)?
To understand the future of identity, it is crucial to grasp the fundamental concepts that make a mobile driver's license a new piece of technology. It is not simply about moving a physical object into a digital format; it is about creating an entirely new, more secure, and **more private way to prove who you are**.
### Beyond a Digital Photo: Defining a True mDL
A true **mobile driver's license** is a dynamic, verifiable credential that is provisioned directly from an issuing authority, like a state's Department of Motor Vehicles (DMV), to a secure application on your mobile device. Unlike a static photo, an mDL contains the same data elements as a physical card and the issuer can update certain elements after issuance. When your device or the verifier has network connectivity, status information (such as revocation or privilege changes) can be checked with the issuer or refreshed from recent synchronizations. As a result, updates can be reflected more easily than with a plastic card.
The core of a true mDL is its ability to be authenticated electronically. When you present your mDL, the data is transmitted securely to a verifier's device, where its authenticity is confirmed through cryptographic checks. This makes it a significant improvement over physical credentials, which are vulnerable to damage, loss, and sophisticated counterfeiting.
### What is the difference between an mDL and a simple digital ID?
The primary difference between a true mDL and a simple digital ID, such as a scanned image or a basic app displaying your license photo, is **verifiability**. A simple digital image is little more than a "rendering of a physical driver's license" that can be easily manipulated with modern graphics tools. It relies on human visual inspection, which is prone to error and cannot detect sophisticated fakes.
A true mDL, by contrast, is designed to be machine-readable and cryptographically secure. The data within the mDL is digitally signed by the issuing authority. When a verifier scans your mDL, their system uses the issuer's public key to confirm that the digital signature is valid and that the data has not been altered since it was issued. This process provides a level of trust and assurance that a simple picture can never achieve.
### The Global Standard: Understanding ISO/IEC 18013-5
The entire global mDL ecosystem is made possible by a single, critical technical standard: [**ISO/IEC 18013-5**](https://www.iso.org/standard/69084.html). Published in 2021, this standard provides the universal framework for how mDLs are structured, secured, and transmitted. It is the "secret sauce" that ensures security, privacy, and, most importantly, **interoperability**.
Interoperability means that an mDL issued in one jurisdiction can be read and verified in another, so long as both systems adhere to the standard. This standard is more than just a technical document; it is a globally agreed-upon trust framework. It solves the fundamental problem of digital identity: how can a verifier in one country trust a digital ID issued by a government in another? Without this common language, mDLs would be a fragmented collection of proprietary apps with limited utility. By defining the rules for data formats, communication protocols (like NFC and QR codes), and security mechanisms, [ISO/IEC 18013-5](https://www.iso.org/standard/69084.html) creates the foundation for mDLs to function as a global utility, much like how the EMV standard allows credit cards to work at terminals worldwide.
## How Do mDLs Actually Work?
The technology behind a mobile driver's license is a sophisticated blend of secure hardware, advanced cryptography, and user-centric design. While the process is complex behind the scenes, it is designed to be simple and intuitive for the end-user.
### The Role of Your Smartphone's Secure Hardware
A mobile driver's license is not stored like a typical photo or document on your phone. Instead, it is anchored to your device's most secure hardware components. On Apple devices, this is the Secure Enclave, and on Android devices, it is a similar component known as the Trusted Execution Environment (TEE).
These secure hardware components are isolated, tamper-resistant microprocessors designed to handle highly sensitive data, such as cryptographic keys and biometric information. When an mDL is created on your phone, its unique private keys are generated and stored within this hardware "fortress". These keys never leave the secure hardware and are not accessible by the main operating system or any other apps. This deep hardware integration makes it incredibly difficult for malware or an attacker to extract the cryptographic material needed to clone your identity, even if the phone's primary software is compromised.
### A Step-by-Step Guide: How Age Verification Works
To understand how an mDL works in practice, let's walk through one of the most common use cases: proving your age to purchase a restricted item.
1. **Initiation**: You arrive at the checkout counter and the cashier asks for ID. You open your mDL application or **digital wallet** on your phone.
2. **Request**: The cashier's point-of-sale system or verification device sends a request to your phone for a specific piece of information: "Is this person over 21?" The request does not ask for your date of birth, name, or address.
3. **Consent**: Your phone displays a consent screen that clearly states, "XYZ Store is requesting proof you are over 21". To proceed, you must give explicit approval by authenticating with your device's security method, such as Face ID, a fingerprint scan, or your PIN. This step ensures you are in full control of the transaction.
4. **Transmission**: Once you approve, your phone generates a temporary QR code for the cashier to scan, or it uses Near Field Communication (NFC) to establish a direct, encrypted communication channel with the verifier's device. You never have to hand over your phone.
5. **Verification**: Your mDL application transmits only the specific data requested—a cryptographically signed "yes"—along with the digital signature from the issuing authority. The verifier's device uses the issuer's public key to instantly confirm that the response is authentic and has not been tampered with. The screen shows a confirmation, and the transaction is complete.
### Online vs. Offline: Verifying Your Identity Anywhere
A key strength of the [ISO/IEC 18013-5](https://www.iso.org/standard/69084.html) standard is its ability to function in both online and offline environments. The age verification scenario described above is a perfect example of an **offline transaction**. The verifier's device does not need an internet connection to the DMV; it only needs a pre-loaded list of trusted public keys from issuing authorities to validate the mDL's signature. This design is crucial for privacy, as it means no central server is involved in the interaction.
**Online verification** is designed for remote situations, such as opening a bank account from your laptop. This process is governed by a complementary standard, [ISO/IEC 18013-7](https://www.iso.org/standard/91154.html), which defines how a verifier (like a bank's website) can establish a secure connection over the internet with your mDL to request and authenticate your identity data, with your consent.
## Key Benefits: Why You'll Want an mDL in Your Digital Wallet
Mobile driver's licenses offer a compelling set of advantages over their physical counterparts, centered on enhanced security, user privacy, and everyday convenience.
### Enhanced Security: How mDLs Prevent Fraud
- **Cryptographic Authentication**: Physical IDs rely on visual security features like holograms, which can be expertly counterfeited. mDLs are protected by strong public-key cryptography, making them virtually impossible to forge. A verifier isn't just looking at an ID; their device is mathematically proving its authenticity.
- **Real-Time Validity**: A physical license could be expired, suspended, or revoked, and a verifier would have no way of knowing. An mDL can be updated or invalidated by the issuing authority in real-time. This ensures that the credential being presented is valid at the exact moment of verification.
- **Biometric Binding**: Access to your mDL is locked behind your phone's biometrics (Face ID, fingerprint) or PIN. This means that even if your phone is stolen, a thief cannot use your mDL without being able to unlock it first.
### Total Privacy Control: Sharing Only the Data You Need
Total privacy control is perhaps the most revolutionary benefit for individuals. The technology is built on the principles of **data minimization** and **[selective disclosure](https://docs.walt.id/concepts/selective-disclosure.md)**.
- **Contextual Information Sharing**: With a physical ID, you reveal all your information—full name, address, date of birth—in every transaction. With an mDL, you only share what is necessary. For age verification, the mDL can simply provide a "yes/no" answer to whether you are over a certain age, without revealing your actual birth date or address.
- **User Consent is Paramount**: You are always in control. An mDL cannot share any information without your explicit consent for each transaction. The consent screen shows you exactly which data points a verifier is requesting before you approve the exchange. This fundamentally shifts the power dynamic of identity verification to the individual.
### Unmatched Convenience: For Citizens and Businesses
- **For Citizens**: Your official ID is always with you on the device you carry everywhere. Administrative tasks like updating your address can often be done remotely through the mDL app, saving a trip to the DMV. If your phone is lost or stolen, the mDL can be remotely deactivated and quickly re-provisioned to a new device, a much faster process than replacing a lost physical card.
- **For Businesses**: The verification process is faster, more accurate, and less susceptible to human error than visually inspecting a physical card. This efficiency improves customer experience. Furthermore, by receiving only the data required for a transaction, businesses reduce their liability and the risk associated with storing sensitive personal information they don't need.
## Where Are Mobile Driver's Licenses Available? A Global Overview
The adoption of mobile driver's licenses is accelerating worldwide, with numerous countries and U.S. states moving from pilot programs to full-scale public rollouts. While the United States is implementing mDLs on a state-by-state basis, many other nations are deploying them as part of broader, nationwide digital identity initiatives. The following table provides a snapshot of the global mDL landscape as of 2026, highlighting key jurisdictions where the technology is live or in advanced stages.
| **Region/Country** | **Status** | **Issuing App / Wallet Compatibility** | **Key Notes** |
|---|---|---|---|
| **NORTH AMERICA** | | | |
| USA - Arizona | Live | Arizona Mobile ID, Apple/Google/Samsung Wallet | One of the earliest adopters, launching in 2021. |
| USA - California | Live | CA DMV Wallet, Apple/Google Wallet | Launched a public pilot program in 2023. |
| USA - Colorado | Live | myColorado, Apple/Google/Samsung Wallet | Integrates mDL with a wide range of other state services. |
| USA - Georgia | Live | Apple/Google/Samsung Wallet | Supports major OEM digital wallets. |
| USA - Louisiana | Live | LA Wallet | The first U.S. state to launch a live mDL in 2018. |
| USA - Maryland | Live | Apple/Google/Samsung Wallet | Launched in 2022 with support for major wallets. |
| **EUROPE** | | | The EU is planning an interoperable digital driving license. |
| Austria | Live | eAusweise | Issued since 2022, part of a broader digital ID suite. |
| France | Live | France Identité | Nationwide rollout began in 2024. |
| Norway | Live | Proprietary App | The first country in the EU/EEA to issue a nationwide mDL. |
| Ukraine | Live | Diia App | Digital ID has the same legal force as its physical counterpart. |
| **ASIA-PACIFIC** | | | |
| Australia (Queensland) | Live | Queensland Digital Licence App | High adoption, with a popular "proof of age" feature. |
| South Korea | Live | PASS App | Developed in collaboration with major telecom operators. |
| **LATIN AMERICA** | | | |
| Mexico (Mexico City) | In Development| N/A | Announced as part of a city-wide digitalization effort. |
## Addressing the Big Questions: Security and Privacy
As with any new identity technology, mDLs raise important questions about security and privacy. A trustworthy system must be robust against attack while also protecting citizens' personal data from misuse. This requires a careful balance and a clear understanding of both the technology's capabilities and its potential risks.
### What are the main mDL security features?
The security of an mDL is built in layers, from the hardware of your phone to the cryptography that protects the data itself. The core security features include:
- **End-to-End Encryption**: When your mDL communicates with a verifier's device, the data is transmitted over a secure, encrypted channel, protecting it from eavesdropping.
- **On-Device Secure Storage**: As discussed, the cryptographic keys that form the foundation of your mDL's security are stored in your phone's hardware-based Secure Enclave or TEE, isolated from the rest of the device.
- **Digital Signatures**: Every piece of data in your mDL is digitally signed by the issuing authority. This makes the data tamper-evident; any modification would invalidate the signature, which a verifier's device would immediately detect.
- **Real-Time Revocation**: If you report your mDL as compromised (for example, if you believe your phone's security has been breached), the issuing authority can remotely revoke the credential, rendering it invalid for any future verification attempts.
- **Biometric Access Control**: You, the holder, must authorize every data-sharing transaction using your device's PIN, fingerprint, or face scan, ensuring that your mDL cannot be used without your active consent.
### Can mDLs be tracked by the government or companies?
Whether mDLs can be tracked is the central question in the privacy debate, and the answer is nuanced. The technology itself is designed to be privacy-preserving, but the policies of the organizations implementing it are what ultimately determine the outcome for users.
The ISO 18013-5 standard was explicitly designed to prevent tracking and enhance privacy. For in-person, offline transactions, the data exchange is peer-to-peer between your device and the verifier's device. No data is sent to the DMV or any other central server, leaving no centralized record of the interaction. The standard itself does not include any mechanism for a central reporting database of mDL usage.
However, civil liberties advocates raise valid concerns about how the technology is implemented in the real world. While the standard is private by design, the specific mDL app provided by a state, or the digital wallet from a tech company, could be built with "phone home" functionality that logs details about your transactions—when, where, and with whom you verified your identity. The risk is not in the protocol, but in the policy layer. A perfectly secure standard can be used within an ecosystem that creates new data trails for corporate data mining or government surveillance. Therefore, the ultimate privacy guarantee depends on the data collection policies of the specific app you use and the laws in your jurisdiction.
For example, in the European Union, [eIDAS2](https://walt.id/eidas2) and the [EUDI Wallet Architecture & Reference Framework](https://eu-digital-identity-wallet.github.io/eudi-doc-architecture-and-reference-framework/2.4.0/architecture-and-reference-framework-main/) explicitly prohibit tracking by wallet providers, require user consent and selective disclosure, and mandate transparency through local transaction logs—ensuring that privacy protections are not only technical but also legally enforceable.
### What happens if I lose my phone?
Losing a phone containing your mDL is less risky than losing a physical wallet. Multiple layers of security protect your identity.
1. **Device-Level Security**: First, your phone is protected by its own lock screen (PIN, fingerprint, or face scan). A thief would need to bypass this primary layer of security to access anything on the device.
2. **App-Level Security**: The mDL itself is a secure application that requires a separate authentication step—your PIN or biometrics—before it can be used to share any data. This provides a crucial second layer of defense.
3. **Remote Deactivation**: In the event your phone is lost or stolen, you have two powerful remote options. First, you can use your device's built-in services, like Apple's Find My or Google's Find My Device, to remotely locate, lock, or even completely wipe the phone's data. More importantly, you can contact your issuing authority (DMV) to have the mDL credential itself remotely revoked. This action makes the mDL on the lost device permanently invalid, even if a thief later manages to unlock the phone.
## The Future of Digital Identity
Mobile driver's licenses are the first step toward a broader ecosystem of digital identity. As the technology matures and adoption grows, mDLs will unlock new use cases and fundamentally change how we interact with businesses and government services.
### Top Use Cases: Travel, Car Rentals, and Financial Services
- **Travel**: Airport security screening is one of the most successful early use cases. The U.S. Transportation Security Administration (TSA) already accepts mDLs for identity verification at security checkpoints in more than 250 airports. This allows for a faster, contactless, and more secure screening process for travelers.
- **Financial Services**: mDLs are poised to revolutionize how banks and financial institutions comply with "Know Your Customer" (KYC) and anti-money-laundering (AML) regulations. Instead of relying on scanned physical documents, which can be easily forged, banks can use mDLs for high-assurance digital identity verification when opening new accounts online or authorizing high-value transactions. This can significantly reduce fraud and streamline the customer onboarding process.
- **Car Rentals**: Vehicle rental is a key area where the ecosystem is still developing. As of 2026, major car rental companies such as Hertz, Avis, and Enterprise do not yet accept mDLs for vehicle rentals; they still require a physical driver's license at the counter. This highlights that while the technology is ready, broad private-sector adoption will take time as businesses update their policies and infrastructure.
### What's Next? The Road to Global Adoption
The next phase of mDL adoption will be driven by **standards-led interoperability** paired with **regulatory pull**. Around the world, policymakers and large institutions are moving from pilots to production wallets and trust frameworks.
In the EU, eIDAS2 requires member states to roll out digital ID wallets and issue high-assurance credentials like the mDL. Beyond Europe, momentum is accelerating: the U.S. expands TSA use with OEM wallet pilots; Australia aligns under [ISO/IEC 18013-5](https://www.iso.org/standard/69084.html); New Zealand enforces an accreditation regime; South Korea runs a live, telco-backed mDL; and Gulf states embed mDL features in e-gov apps.
Adoption typically arrives in waves:
- (1) government-anchored credentials like mDLs seed wallets with high-assurance data;
- (2) regulated sectors (airports, finance, mobility) adopt selective-disclosure flows that lower liability;
- (3) platform ecosystems (OEM wallets, cloud identity, IAM vendors) normalize UX and accelerate verifier uptake.
The ultimate vision is a future where secure digital credentials on your phone serve as a universal key for proving your identity—privately, securely, and seamlessly across both the physical and digital worlds.
## Become an mDL Issuer, Verifier, or Wallet Provider with walt.id
Learn how to issue, verify, and store mobile driver’s licenses (mDLs) quickly and easily. Choose the open-source Community Stack (Apache-2.0) or the production-ready Enterprise Stack from walt.id, trusted by :traction-devs{} developers and organizations worldwide.
### Product Editions
- [The Community Stack](https://docs.walt.id/community-stack/home/the-community-stack.md) (Apache-2.0) — trusted open source multi-platform libs, powerful APIs and easy-to-use white label apps for digital identity and wallets.
- [The Enterprise Stack](https://docs.walt.id/enterprise-stack/home/the-enterprise-stack.md) - solution for building secure, compliant and robust identity and wallet solutions or platforms at scale.
### Guides
- [How to issue a Mobile Driver's License (mDL)](https://docs.walt.id/community-stack/issuer/credential-issuance/mdl-oid4vc.md)
- [How to veify a Mobile Driver's License (mDL)](https://docs.walt.id/community-stack/verifier/credential-verification/mdl-oid4vc.md)
- [How to accept a Mobile Driver's License (mDL) in a wallet](https://docs.walt.id/community-stack/wallet/credential-exchange/guides/accept-iso-18013-5-mdl-oid4vci.md)
- [How to present a Mobile Driver's License (mDL) from a wallet](https://docs.walt.id/community-stack/wallet/credential-exchange/guides/present-digital-credentials-oid4vp.md)
--------------------------------------------------------------------------------
PAGE: Selective Disclosure
URL: https://docs.walt.id/concepts/selective-disclosure
RAW: https://docs.walt.id/concepts/selective-disclosure.md
STACK: Concepts (stack-agnostic, applies to both Community and Enterprise Stack)
--------------------------------------------------------------------------------
# Selective Disclosure & SD-JWTs: The Complete Guide to Privacy-Preserving Digital Identity
**Selective disclosure** is a core capability in modern digital identity, enabling users to reveal only the information that is strictly necessary. This guide explains what selective disclosure is, why it matters for privacy-preserving digital identity, and how it works across credential formats such as [SD-JWT VCs](https://docs.walt.id/concepts/digital-credentials/sd-jwt-vc.md), [mDL](https://docs.walt.id/concepts/digital-credentials/mdoc-mdl-iso.md), and mono-claim credentials.
**In this guide, you will learn** the key concepts behind selective disclosure, the technical flows used during issuance and verification, and practical guidance for choosing the right approach when implementing real-world digital identity systems.
## What is Selective Disclosure?
Selective disclosure is one of the most important capabilities in [decentralized identity](https://docs.walt.id/concepts/decentralized-identity.md) and Verifiable Credential ([VC](https://docs.walt.id/concepts/digital-credentials.md)) ecosystems. It enables a holder to reveal **only the minimum amount of information required** for a transaction, nothing more.
In today’s digital world, personal data is copied, stored, sold, breached, and shared far beyond user expectations. Centralized and federated identity systems force users to overshare information. For example, showing a driver’s licence at a bar reveals your **name, address, licence number, and date of birth,** even though the verifier only needs to know that you are **over 18.**
Selective disclosure reverses this pattern. Instead of the verifier receiving the entire credential, users share only the specific claims that matter:
* Prove age without revealing a date of birth
* Prove residency without showing a full address
* Prove credential validity without sharing unnecessary attributes
Selective disclosure solves a fundamental privacy and data minimization problem: how to prove something about yourself **without exposing more personal data than necessary**. It directly aligns with modern regulations such as GDPR, CCPA, and [eIDAS2](https://walt.id/eidas2), all of which require organizations to limit the amount of data they collect and process.
Selective disclosure also depends on three actors working together:
- **Issuer** – Creates and signs the credential.
- **Holder** – Stores the credential inside a digital identity wallet.
- **Verifier** – Requests only the specific attributes needed for an interaction.
These roles form the foundation of decentralized identity ecosystems. (For a deeper overview of issuers, holders, and verifiers, see the [Decentralized Identity Playbook](https://walt.id/white-paper/decentralized-identity-playbook) by walt.id.)
## Why Selective Disclosure Matters: Benefits for Users and Enterprises
Selective disclosure is central to the global shift toward data minimization. Identity systems have moved from centralized models, where every service stored its own user database and repeatedly exposed people to breaches and redundant KYC, to federated logins that improved usability but concentrated power and enabled cross-service tracking. Decentralized and self-sovereign identity introduced a new approach where users hold credentials in their own wallet and verifiers rely on cryptographic proofs. In this model, selective disclosure becomes a native feature because holders share only the attributes required, not their entire identity document.
### For Individuals: Privacy and Control
Selective disclosure gives people meaningful control over their personal data:
* Only essential attributes are shared.
* Less data is exposed, reducing tracking and correlation.
* Minimal transmission and storage of PII lowers breach and identity theft risk.
### For Enterprises and Governments: Compliance and Efficiency
Organizations benefit by requesting only what they need:
* Strong alignment with GDPR, CCPA, and eIDAS2.
* Reduced liability from storing unnecessary personal data.
* Increased trust and smoother onboarding because users see exactly what is being requested.
Selective disclosure improves privacy, reduces risk, and enables modern identity ecosystems built on minimal, purpose-based data sharing.
## How Selective Disclosure Actually Works
At its core, selective disclosure relies on modern cryptographic techniques that allow information to remain hidden until the holder intentionally reveals it. Hash functions, salts, and digital signatures ensure that each claim inside a credential can be individually committed to and later verified without exposing the entire dataset.
Protocols such as OpenID for Verifiable Presentations ([OID4VP](https://docs.walt.id/concepts/data-exchange-protocols/openid4vp.md)) and its query language, [DCQL](https://docs.walt.id/concepts/data-exchange-protocols/dcql.md), enable verifiers to request only the precise fields necessary for an interaction. These protocols define how a digital wallet receives a request, evaluates which credentials can satisfy it, and constructs a privacy-preserving presentation containing only the required disclosures. An end-to-end flow with a code example will be presented in a later section to illustrate how this works in practice.
### The Credential Standards That Enable Selective Disclosure
Different credential formats support selective disclosure in different ways. Some, like SD-JWT VCs, enable issuers to choose which claims can be revealed on a per-attribute basis, while others, like ISO mDL/mdoc, hide all data by default and only disclose specific elements during an authenticated session. Mono-claim credentials take a simpler approach by encoding only a single attribute per credential, and [W3C VCs](https://docs.walt.id/concepts/digital-credentials/verifiable-credentials-w3c.md) can also support selective disclosure when signed using SD-JWT.
This section explains how each format works, how selective disclosure is achieved, and how these formats differ from one another.
#### SD-JWT VC (IETF)
Selective Disclosure JWT VCs enable claim-level selective disclosure through a hashing-based mechanism defined by the emerging IETF SD-JWT specification. Instead of storing plaintext values inside the credential, the issuer hashes each selectively disclosable claim and provides the holder with separate “disclosure” objects containing the salted plaintext values. This means a verifier cannot read any claim unless the holder intentionally reveals it. You can view the official IETF SD-JWT standard here or read our guide on the credential type here.
The SD-JWT issuance process works as follows:
1. The issuer prepares the VC payload, including context, types, schemas, and claims.
2. For each claim that should be selectively disclosable, the issuer generates a random salt, concatenates it with the claim name and value, encodes it to form a disclosure, and then hashes that disclosure.
3. These hashed claim values are stored in the SD-JWT, and optional decoy hashes can be added to obscure the true number of claims.
4. The issuer signs the SD-JWT using JWS (for example, EdDSA) and sends the signed credential plus all disclosures to the holder using for example the [OID4VCI](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md) exchange standard.

For example, an SD-JWT VC for age verification might contain a full date of birth and other personal data, but the holder only discloses a single derived claim such as `age_over_18: true` to the verifier.
Verification follows a similar pattern. A verifier requests specific attributes using common protocols like OID4VP, and the holder returns only the relevant disclosures along with the SD-JWT. The verifier re-hashes each disclosure, compares it to the stored hashed value, and verifies the issuer’s signature. If everything matches, the verifier knows the claim is valid and untampered. You can refer to the OID4VP guide for further detail on how a typical verification flow looks like.
walt.id’s implementation follows the IETF specification closely and is continuously updated to align with new versions of the standard as they are published.
#### mDL/mdoc (ISO 18013-5 and 18013-7)
ISO mobile driving licences (mDL) and mobile documents (mdoc) implement selective disclosure very differently from SD-JWT VC (IETF). Instead of hashing selectively disclosable claims, **all data elements are hidden by default**. A verifier can only access requested attributes during an authenticated, device-bound session initiated through NFC, BLE, QR code scanning or via OID4VP with 18013-7.
For example, an mDL could disclose only a single attribute such as `age_over_18` or `driving_privilege: yes`, while keeping all other personal details—like full name, address, or licence number—hidden by default.
In this model, the mdoc proves possession of its keys and authorizes the release of specific data elements during the session. It is highly optimized for offline or proximity-based interactions such as border control, airport checkpoints, police stops, and age-restricted purchases.
**Characteristics of ISO mDL/mdoc:**
* Selective disclosure at the data-element level
* Strong device binding (proof the data is coming from the legitimate device)
* Ephemeral sessions that protect against correlation
* Use of ISO rather than JWT/JWS cryptographic primitives
Where SD-JWT is ideal for web-based, API-driven online verification flows, mDLs excel in controlled, physical environments with secure device-to-verifier sessions, but can also support online verification through ISO 18013-7 and OID4VP.
#### Mono-Claim Credentials
Mono-claim credentials achieve selective disclosure through **credential granularity** instead of cryptography. Each credential contains exactly one attribute, meaning the holder can share or withhold individual attributes simply by choosing which credential to present. Because they rely on this structural approach rather than a specific cryptographic format, mono-claim credentials are largely standard-independent and can be issued as W3C VCs, SD-JWT VCs, or other compatible credential types.
**Example of an age-verification credential:**
```json
{
"@context": ["[https://www.w3.org/2018/credentials/v1](https://www.w3.org/2018/credentials/v1)"],
"type": ["VerifiableCredential", "VerifiableAttestation", "ProofOfAge"],
"credentialSubject": {
"id": "did:key:z6MkrHKzgsahxBLyNAbLQyB1pcWNYC9GmywiWPgkrvntAZcj",
"over18": "true"
},
"issuer": {
"id": "did:key:z6MkrHKzgsahxBLyNAbLQyB1pcWNYC9GmywiWPgkrvntAZcj",
"name": "Government of Anytown"
},
"issuanceDate": "2021-08-31T00:00:00Z"
}
```
This model is simple, interoperable, and works with any VC stack. While the example is shown in W3C VC format, mono-claim credentials are not tied to a single standard and can equally be issued using SD-JWT VCs or other compatible credential frameworks. This model is especially useful when attributes rarely need to be combined and when minimal implementation complexity is important.
#### W3C VC With SD-JWT Signature
A W3C Verifiable Credential can also support selective disclosure when its proof layer uses an SD-JWT signature. In this model, the credential still follows the W3C VC Data Model, but its proof section adopts SD-JWT as the underlying cryptographic mechanism for hashing claims and supplying disclosures.
This approach is ideal for issuers who want to use the W3C VC model while adopting SD-JWT as the privacy-preserving signature suite. It combines the interoperability of the W3C VC ecosystem with the selective disclosure capabilities of SD-JWT.
### How To Decide Which Credential Standard Is Best
Selecting the right credential format depends on the use case, the environment in which verification occurs, and the level of privacy required. To help readers evaluate their options, this section will include a comparison table summarizing the strengths and trade-offs of SD-JWT VCs, mDL/mdoc, mono-claim credentials, and W3C VCs with SD-JWT signatures.
Because some advanced schemes like BBS+ and ZKPs are still emerging and not yet widely deployed in production, they will be mentioned briefly at the bottom with links to future articles once dedicated coverage is available.
| Feature / Requirement | SD-JWT VC (IETF) | mDL / mdoc (ISO 18013-5/7) | Mono-Claim Credentials | W3C VC + SD-JWT Signature |
|-------------------------------|-------------------------------------------|------------------------------------------|-----------------------------------------|---------------------------------------|
| **Granularity of Disclosure** | Claim-level (highly granular) | Data-element level | Single attribute per credential | Claim-level (highly granular) |
| **Default Visibility** | All fields visible unless disclosed | Everything hidden by default | Entire credential visible (one claim) | All fields visible unless disclosed |
| **Interaction Model** | Online: Web/API-based | Offline: Session (NFC/BLE/QR) or Online: Web via ISO 18013-7 | Online or Offline | Online: Web/API-based |
| **Best For** | Online verification, EUDI Wallets, enterprise flows | Border control, mobility, offline checks | Simple attestations: age, nationality, membership | Web-native credentials needing SD behavior |
| **Production Maturity** | Emerging but rapidly adopted | Deployed in US & global pilots | Mature (stable standards, production deployments) | Emerging |
| **Supported by walt.id** | Yes | Yes | Yes | Yes |
## Key Use Cases and Examples
Selective disclosure is already powering real-world digital ID interactions by allowing users to reveal only the specific information needed for a transaction. Common use cases include:
- Age verification (prove you are over 18 without sharing your date of birth)
- Mobile Driver’s License (mDL) (share only required attributes like “over 21” or “valid licence”)
- Mono-claim credentials such as “Over 18,” “Resident of X,” or “Employed by Y,” issued as single-purpose Verifiable Credentials. These can also be leveraged for address verification (confirm residency without revealing full address details) or Salary verification (prove income range without exposing exact salary)
## Global Initiatives & Regulations Driving Selective Disclosure Adoption
Selective disclosure is being accelerated by major global digital identity programs. Mobile Driver’s Licenses (mDLs) and ISO mdocs are already deployed across many US states and are expanding internationally, enabling privacy-preserving ID checks using data-element–level disclosure. In the EU, eIDAS2 and the EUDI Wallet initiative mandate support for selective disclosure through both SD-JWT VCs (IETF) and ISO mDL/mdoc, establishing these as the foundational standards for next-generation digital identity. Together, these efforts are setting worldwide expectations for interoperable, privacy-first credential sharing
## End-to-End Selective Disclosure Example (with Code)
This section walks through a complete SD-JWT VC flow from issuance to verification, demonstrating how selective disclosure works in practice. At each step, we explain the cryptographic mechanics that make privacy-preserving credential sharing possible.
### Understanding How SD-JWT Hashes Work
Before diving into the code, it's essential to understand how SD-JWT achieves selective disclosure through cryptographic hashing.
When an issuer creates an SD-JWT VC, selectively disclosable claims are **not stored in plaintext** inside the credential. Instead, each claim undergoes a transformation:
1. **Salt Generation**: The issuer generates a cryptographically random salt for each selectively disclosable claim.
2. **Disclosure Creation**: The salt, claim name, and claim value are combined into a JSON array: `[salt, claim_name, claim_value]`.
3. **Base64url Encoding**: This array is encoded as a base64url string, creating the "disclosure."
4. **Hash Computation**: The disclosure is hashed using SHA-256, and the resulting hash is base64url encoded.
5. **Storage**: The hash (not the plaintext) is stored in the credential's `_sd` array. The disclosures are provided separately to the holder.
**Example Transformation:**
For a claim `"birthdate": "1940-01-01"`, the issuer:
```
Disclosure: ["zqdrNfRPzxYivbvZevEh1w", "birthdate", "1940-01-01"]
↓ Base64url encode
Encoded: WyJ6cWRyTmZSUHp4WWl2YnZaZXZFaDF3IiwiYmlydGhkYXRlIiwiMTk0MC0wMS0wMSJd
↓ SHA-256 hash → Base64url encode
Hash: vyCkk8nkNZcSNBqzLyhSzDp1AuwnS911ukrFgmFZPFI
```
The credential payload contains only the hash in `_sd`:
```json
{
"given_name": "John",
"family_name": "Doe",
"_sd": ["vyCkk8nkNZcSNBqzLyhSzDp1AuwnS911ukrFgmFZPFI"]
}
```
During verification, the verifier re-hashes any disclosures provided by the holder and compares them against the stored hashes to confirm validity.
### Step 1: Issue an SD-JWT VC with Selective Disclosure
**Product:** [walt.id Issuer API](https://docs.walt.id/community-stack/issuer/getting-started.md) (Open Source)
We use the walt.id Issuer API to create an identity credential where the `birthdate` and `family_name` fields are selectively disclosable—meaning the holder can later choose whether to reveal them.
The following request uses an example signing key and issues a credential with selective disclosure configured. You can run this directly against the [hosted demo issuer](https://issuer.demo.walt.id) or [set up your own instance](https://docs.walt.id/community-stack/issuer/setup.md):
```bash
curl -X 'POST' \
'https://issuer.demo.walt.id/openid4vc/sdjwt/issue' \
-H 'accept: text/plain' \
-H 'Content-Type: application/json' \
-d '{
"issuerKey": {
"type": "jwk",
"jwk": {
"kty": "OKP",
"d": "mDhpwaH6JYSrD2Bq7Cs-pzmsjlLj4EOhxyI-9DM1mFI",
"crv": "Ed25519",
"kid": "Vzx7l5fh56F3Pf9aR3DECU5BwfrY6ZJe05aiWYWzan8",
"x": "T3T4-u1Xz3vAV2JwPNxWfs4pik_JLiArz_WTCvrCFUM"
}
},
"issuerDid": "did:key:z6MkjoRhq1jSNJdLiruSXrFFxagqrztZaXHqHGUTKJbcNywp",
"credentialConfigurationId": "identity_credential_vc+sd-jwt",
"credentialData": {
"given_name": "John",
"family_name": "Doe",
"email": "johndoe@example.com",
"birthdate": "1940-01-01",
"is_over_18": true,
"is_over_21": true
},
"mapping": {
"id": "",
"iat": "",
"nbf": "",
"exp": ""
},
"selectiveDisclosure": {
"fields": {
"birthdate": { "sd": true },
"family_name": { "sd": true }
},
"decoyMode": "NONE",
"decoys": 0
},
"authenticationMethod": "PRE_AUTHORIZED"
}'
```
**Response (Credential Offer URL):**
```
openid-credential-offer://?credential_offer_uri=https%3A%2F%2Fissuer.demo.walt.id%2Fdraft13%2FcredentialOffer%3Fid%3D080726d1-70e0-426c-ab06-10e41225853f
```
### Step 2: Claim the Credential in a Wallet
**Product:** [walt.id Web Wallet](https://docs.walt.id/community-stack/wallet/getting-started.md) (Open Source)
The holder uses a wallet to claim the credential using the offer URL. The easiest way to try this is using the **walt.id Demo Wallet** hosted at [wallet.demo.walt.id](https://wallet.demo.walt.id). You can also [run the wallet locally](https://docs.walt.id/community-stack/wallet/setup.md) or build your own wallet using the [Wallet API](https://docs.walt.id/community-stack/wallet/credential-exchange/overview.md).
1. **Create an account** at [wallet.demo.walt.id](https://wallet.demo.walt.id) (or log in if you already have one).
2. Click the **"Receive"** button at the top of the home screen.
3. **Paste the Credential Offer URL** (from Step 1) into the text field.
4. Review and **accept** the credential.
The credential is now stored in your wallet.
**Note:**
**Tip:** You can also scan the credential offer URL as a QR code if you generate one from the URL.
You can learn more about the different [credential delivery methods here](https://docs.walt.id/community-stack/issuer/capabilities/credential-delivery.md).
**What the wallet receives and stores:**
The wallet now holds:
- The signed SD-JWT credential (containing hashes in `_sd`)
- All disclosures provided by the issuer
The credential payload stored in the wallet looks like this:
```json
{
"given_name": "John",
"email": "johndoe@example.com",
"is_over_18": true,
"is_over_21": true,
"id": "urn:uuid:16db85c2-6a60-4339-94f8-617ef7a7852f",
"iat": 1765984477,
"nbf": 1765984477,
"exp": 1797520477,
"_sd_alg": "sha-256",
"iss": "did:key:z6MkjoRhq1jSNJdLiruSXrFFxagqrztZaXHqHGUTKJbcNywp",
"cnf": {
"jwk": {
"kty": "EC",
"crv": "P-256",
"kid": "g5NZkDNcGXI06zy7mFLG09PvFBwdBEDO7I3bn78etlM",
"x": "woHr8cHiXFB4HYBnIqqeudpZme6XkmkwVgLg9QqlGs0",
"y": "OtX0EgOF28c6-FYpHfr5xceyGjllH75CV6S92AwBTcU"
}
},
"vct": "https://issuer.demo.walt.id/draft13/identity_credential",
"_sd": [
"VcCZB3lnwdRmwsLoiAhXET3U_i09mS-gKO_STqyK9n0",
"jAyj3j4i8mPCR98nV1jPccTqvqTdZ0tSfmHhQ1HsdFw"
]
}
```
Notice that `birthdate` and `family_name` are **not visible** in the payload, only their hashes appear in `_sd`. The holder (wallet) has the corresponding disclosures that can reveal these values when needed.
### Step 3: Verify the Credential (Request Specific Claims)
**Product:** [walt.id Verifier API](https://docs.walt.id/community-stack/verifier/getting-started.md) (Open Source)
Now a verifier wants to confirm the holder is over 18 and learn their family name—but does **not** need their birthdate. Using the walt.id Verifier API, we create a presentation request that specifies exactly which claims are required. You can run this against the [hosted demo verifier](https://verifier.demo.walt.id) or [set up your own instance](https://docs.walt.id/community-stack/verifier/setup.md):
```bash
curl -X 'POST' \
'https://verifier.demo.walt.id/openid4vc/verify' \
-H 'accept: */*' \
-H 'authorizeBaseUrl: openid4vp://authorize' \
-H 'responseMode: direct_post' \
-H 'successRedirectUri: https://example.com/success?id=$id' \
-H 'errorRedirectUri: https://example.com/error?id=$id' \
-H 'Content-Type: application/json' \
-d '{
"vp_policies": ["presentation-definition"],
"request_credentials": [
{
"vct": "https://issuer.demo.walt.id/draft13/identity_credential",
"format": "dc+sd-jwt",
"input_descriptor": {
"id": "identity_check",
"constraints": {
"fields": [
{
"path": ["$.family_name"],
"filter": { "type": "string" }
},
{
"path": ["$.is_over_18"],
"filter": { "type": "boolean", "const": true }
}
],
"limit_disclosure": "required"
}
}
}
]
}'
```
**Response (Verification Request URL):**
```
openid4vp://authorize?response_type=vp_token&client_id=https%3A%2F%2Fverifier.demo.walt.id...&state=HMUgYMpanNr8...
```
### Step 4: Present the Credential with Selective Disclosure
**Product:** [walt.id Web Wallet](https://docs.walt.id/community-stack/wallet/credential-exchange/overview.md) (Open Source)
Using the **walt.id Demo Wallet** at [wallet.demo.walt.id](https://wallet.demo.walt.id), you can fulfill the verification request:
1. In the wallet, click the **"Scan to receive or present credentials"** button (top right corner).
2. **Paste the Verification Request URL** (from Step 3) into the text field below the QR scanner.
3. The wallet will show which credentials match the request and **which claims will be disclosed**.
4. **Select which attributes to disclose** — you can choose to reveal or hide the selectively disclosable fields (`family_name` and `birthdate`).
5. Review and **approve** the presentation.
**Try it yourself:** Since the verifier's request requires `family_name` and `is_over_18`, the verification outcome depends on your selection:
- **Success**: If you disclose `family_name` (and `is_over_18` is already visible), verification passes.
- **Failure**: If you choose **not** to disclose `family_name`, verification will fail because the required field is missing.
- **Privacy preserved**: Whether you disclose `birthdate` or not has no effect on the result—it wasn't requested by the verifier.
**What happens during presentation:**
The wallet constructs the `vp_token` as follows:
```
~~
```
Where:
- `` is the original signed credential (with hashes)
- `` is **only** the disclosure for `family_name`
- `` proves the holder controls the credential
The `birthdate` disclosure is **omitted**—the verifier cannot learn its value.
Example:
```
eyJraWQiOiJkaWQ6a2V5Ono2TWtqb1JocTFqU05KZExpcnVTWHJGRnhhZ3FyenRaYVhIcUhHVVRLSmJjTnl3cCN6Nk1ram9SaHExalNOSmRMaXJ1U1hyRkZ4YWdxcnp0WmFYSHFIR1VUS0piY055d3AiLCJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFZERTQSJ9.eyJnaXZlbl9uYW1lIjoiSm9obiIsImVtYWlsIjoiam9obmRvZUBleGFtcGxlLmNvbSIsImlzX292ZXJfMTgiOnRydWUsImlzX292ZXJfMjEiOnRydWUsImlkIjoidXJuOnV1aWQ6MTZkYjg1YzItNmE2MC00MzM5LTk0ZjgtNjE3ZWY3YTc4NTJmIiwiaWF0IjoxNzY1OTg0NDc3LCJuYmYiOjE3NjU5ODQ0NzcsImV4cCI6MTc5NzUyMDQ3NywiX3NkX2FsZyI6InNoYS0yNTYiLCJpc3MiOiJkaWQ6a2V5Ono2TWtqb1JocTFqU05KZExpcnVTWHJGRnhhZ3FyenRaYVhIcUhHVVRLSmJjTnl3cCIsImNuZiI6eyJqd2siOnsia3R5IjoiRUMiLCJjcnYiOiJQLTI1NiIsImtpZCI6Imc1TlprRE5jR1hJMDZ6eTdtRkxHMDlQdkZCd2RCRURPN0kzYm43OGV0bE0iLCJ4Ijoid29IcjhjSGlYRkI0SFlCbklxcWV1ZHBabWU2WGtta3dWZ0xnOVFxbEdzMCIsInkiOiJPdFgwRWdPRjI4YzYtRllwSGZyNXhjZXlHamxsSDc1Q1Y2UzkyQXdCVGNVIn19LCJ2Y3QiOiJodHRwczovL2lzc3Vlci5kZW1vLndhbHQuaWQvZHJhZnQxMy9pZGVudGl0eV9jcmVkZW50aWFsIiwiX3NkIjpbIlZjQ1pCM2xud2RSbXdzTG9pQWhYRVQzVV9pMDltUy1nS09fU1RxeUs5bjAiLCJqQXlqM2o0aThtUENSOThuVjFqUGNjVHF2cVRkWjB0U2ZtSGhRMUhzZEZ3Il19.FF9kDs-vxqsd6serp-5TouRmw3l0JBAINhNcBbATb-3_kEHXSSjURJv4UmVLTc5vV8HDRwmHllfteLb-pR5_Bg~WyJZYmp2aDVjQUg5QlBiVmpHMWJJYnV3IiwiZmFtaWx5X25hbWUiLCJEb2UiXQ~eyJraWQiOiJnNU5aa0ROY0dYSTA2enk3bUZMRzA5UHZGQndkQkVETzdJM2JuNzhldGxNIiwidHlwIjoia2Irand0IiwiYWxnIjoiRVMyNTYifQ.eyJpYXQiOjE3NjU5ODY5ODYsImF1ZCI6Imh0dHBzOi8vdmVyaWZpZXIuZGVtby53YWx0LmlkL29wZW5pZDR2Yy92ZXJpZnkiLCJub25jZSI6ImEzMDUwZTQ1LWU0MGQtNDQzNy1hMjkyLWY2YzBhOTFhY2E2ZiIsInNkX2hhc2giOiJncmF6UGNqaGR5RWxvOFpycllfV3VUX1JTRzNxWkhaUU9vWW9GVjgwajFjIn0.rpgfWZTLLfw55hbEux69wbNmZLa4p0Wb2EPMzW4Y5ULOX5VGH_fqoeA90jugsMXefu65uA86VB08TjSVV_jGEg
```
### Step 5: Verification Result
The verifier receives the presentation and performs these checks:
1. **Signature verification**: Confirms the SD-JWT was signed by a trusted issuer.
2. **Disclosure verification**: Re-hashes each provided disclosure and matches against `_sd`.
3. **Key binding verification**: Confirms the presenter holds the credential's private key.
```bash
curl -X 'GET' \
'https://verifier.demo.walt.id/openid4vc/session/{session-id}' \
-H 'accept: */*'
```
**Note:**
You can get the session id from the id query param when you got redirected to the success URL during
presentation.
## Get Started with Selective Disclosure
Ready to implement selective disclosure in your own identity solution? Explore the resources below to dive deeper into credential formats and start building with walt.id's open-source stack.
### Learn More About Credential Formats
Each credential format enables selective disclosure differently. Choose the one that fits your use case:
- [SD-JWT VC Guide](https://docs.walt.id/concepts/digital-credentials/sd-jwt-vc.md)
- [mDL / mdoc Guide](https://docs.walt.id/concepts/digital-credentials/mdoc-mdl-iso.md)
- [W3C VC Guide](https://docs.walt.id/concepts/digital-credentials/verifiable-credentials-w3c.md)
### Start Building with walt.id
The walt.id Identity Stack provides everything you need to issue, hold, and verify credentials with selective disclosure support:
- [Issuer Getting Started](https://docs.walt.id/community-stack/issuer/getting-started.md) - Issue SD-JWT VCs, W3C VCs, and mDL credentials with configurable selective disclosure
- [Verifier Getting Started](https://docs.walt.id/community-stack/verifier/getting-started.md) - Request and verify specific claims using OID4VP and presentation definitions
- [Wallet Getting Started](https://docs.walt.id/community-stack/wallet/getting-started.md) - Store credentials and present them with user-controlled disclosure selection
All components listed are **open source** and available on [GitHub](https://github.com/walt-id/waltid-identity).
--------------------------------------------------------------------------------
PAGE: Credential Status & Revocation
URL: https://docs.walt.id/concepts/credential-lifecycle/credential-status-and-revocation
RAW: https://docs.walt.id/concepts/credential-lifecycle/credential-status-and-revocation.md
STACK: Concepts (stack-agnostic, applies to both Community and Enterprise Stack)
--------------------------------------------------------------------------------
# Credential Status & Revocation
The validity of digital credentials are fundamental to make digital identity work. Ensuring
that verifiable credentials are up-to-date and valid without compromising user privacy is essential. Let's understand
why
we need a credential status, explore possible solutions, and learn more about [the Bitstring
Status List v1.0](https://www.w3.org/TR/vc-bitstring-status-list/).
## Why we need credential status
Digital credentials, such as certificates, badges, and IDs, must have mechanisms to indicate their current
status—whether they are active, suspended, or revoked. The ability to check the status of a credential is crucial for
several reasons:
1. **Security**: Verifying that a credential has not been revoked or tampered with is vital to prevent fraud and misuse.
2. **Trust**: Ensuring that only valid credentials are accepted maintains trust in digital identity systems.
3. **Compliance**: Many regulatory frameworks require mechanisms to manage and verify the status of digital credentials.
Traditional methods like Certificate Revocation Lists (CRLs) and the Online Certificate Status Protocol (OCSP) have
limitations, including potential privacy breaches and inefficiency in handling large volumes of data.
## Possible Solutions
Addressing the need for an efficient and privacy-preserving way to manage credential status has led to various
solutions:
- **Certificate Revocation Lists (CRLs)**: Lists employed to manage the revocation status of digital certificates. They
can
be large and cumbersome to distribute.
- **Online Certificate Status Protocol (OCSP)**: Provides real-time status information but can expose user actions to
tracking.
Each of these solutions has trade-offs, particularly regarding privacy and scalability. To enhance privacy and
efficiency, new mechanisms like the Bitstring Status List have been developed.
## Bitstring Status List Explained
The Bitstring Status List, specified [here](https://www.w3.org/TR/vc-bitstring-status-list/), proposes a novel approach
to managing
credential status using bitstrings. This method offers several advantages over traditional solutions:
- **Privacy**: By bundling the status of many credentials into a single list, the Bitstring Status List mitigates
tracking
risks associated with querying individual credential statuses.
- **Efficiency**: The use of bitstrings and data compression techniques like GZIP ensures that the status information is
highly space-efficient and easy to distribute, even for a large userbase.
In essence, the status of a large number of [VCs](https://docs.walt.id/concepts/digital-credentials.md) can be grouped into a single list using bitstrings. A bitstring is a
sequence of bits, where each bit represents the status of one credential. If a bit is set (1), the credential is revoked
or suspended; if unset (0), the credential is active.
### Example StatusListCredentials
To illustrate how the Bitstring Status List works, let's look at some practical examples.
#### Simple Status List Credential
A simple status list credential is a straightforward implementation of the Bitstring Status List mechanism. It
associates a single status list entry with a verifiable credential.
```json
{
"@context": [
"https://www.w3.org/ns/credentials/v2"
],
"id": "https://example.com/credentials/status/3",
"type": [
"VerifiableCredential",
"BitstringStatusListCredential"
],
"issuer": "did:example:12345",
"validFrom": "2021-04-05T14:27:40Z",
"credentialSubject": {
"id": "https://example.com/status/3#list",
"type": "BitstringStatusList",
"statusPurpose": "revocation",
"encodedList": "uH4sIAAAAAAAAA-3BMQEAAADCoPVPbQwfoAAAAAAAAAAAAAAAAAAAAIC3AYbSVKsAQAAA"
}
}
```
This example shows a minimal setup where a single status list oversees the revocation status of credentials. The
encodedList property contains the compressed bitstring representing the status of numerous credentials, ensuring
efficient data handling and privacy preservation.
#### Multiple Status Purposes
The Bitstring Status List mechanism can also support multiple status purposes within a single credential. This
capability enhances flexibility by allowing various types of status updates (e.g., revocation, suspension) to be managed
together.
```json
{
"@context": [
"https://www.w3.org/ns/credentials/v2"
],
"id": "https://example.com/credentials/23894672394",
"type": [
"VerifiableCredential"
],
"issuer": "did:example:12345",
"issuanceDate": "2021-04-05T14:27:42Z",
"credentialStatus": [
{
"id": "https://example.com/credentials/status/5#94567",
"type": "BitstringStatusListEntry",
"statusPurpose": "revocation",
"statusListIndex": "94567",
"statusListCredential": "https://example.com/credentials/status/5"
},
{
"id": "https://example.com/credentials/status/5#12345",
"type": "BitstringStatusListEntry",
"statusPurpose": "suspension",
"statusListIndex": "12345",
"statusListCredential": "https://example.com/credentials/status/5"
}
],
"credentialSubject": {
"id": "did:example:6789",
"type": "Person"
}
}
```
This example demonstrates a more complex implementation where a single verifiable credential can be marked as both
revoked and suspended. This dual-purpose credential status allows managing different characteristics of credential
status in a consolidated manner, further enhancing flexibility and efficiency.
## Credential Status and Revocation with walt.id
Using walt.id's libs and services, you can issue and verify credentials leveraging the Bistring StatusList standard. At the
moment, our policies support checking for any state in credentials. Learn more [here](https://docs.walt.id/community-stack/verifier/credential-verification/policies/overview.md).
- [Issuer API](https://docs.walt.id/community-stack/issuer/getting-started.md)
- [Verifier API](https://docs.walt.id/community-stack/verifier/getting-started.md)
--------------------------------------------------------------------------------
PAGE: Data Exchange Protocols
URL: https://docs.walt.id/concepts/data-exchange-protocols
RAW: https://docs.walt.id/concepts/data-exchange-protocols.md
STACK: Concepts (stack-agnostic, applies to both Community and Enterprise Stack)
--------------------------------------------------------------------------------
# Data Exchange Protocols for Digital Identity (SSI)
Different authentication and data exchange protocols are used to securely transfer identity data (e.g. [VCs](https://docs.walt.id/concepts/digital-credentials.md), VPs) between
parties (e.g. from an Issuer to a Holder). They typically establish a mutually authenticated and encrypted data channel
between the communicating parties.
### Protocols
- **OpenID4VCI (Issuance)**: [What is OpenID4VCI? The Developer's Guide](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md)
- **OpenID4VP (Presentation)**: [What is OpenID4VP? The Developer's Guide](https://docs.walt.id/concepts/data-exchange-protocols/openid4vp.md)
- **DC-API (Browser-Based)**: [What is DC-API? The Developer's Guide](https://docs.walt.id/concepts/data-exchange-protocols/dc-api.md)
- **HAIP (High Assurance Profile)**: [What is the HAIP Profile? The Developer's Guide](https://docs.walt.id/concepts/data-exchange-protocols/haip.md)
- **DCQL (Query Language)**: [DCQL: The Developer's Guide](https://docs.walt.id/concepts/data-exchange-protocols/dcql.md)
--------------------------------------------------------------------------------
PAGE: What is OpenID4VCI? The Developer's Guide (2026)
URL: https://docs.walt.id/concepts/data-exchange-protocols/openid4vci
RAW: https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md
STACK: Concepts (stack-agnostic, applies to both Community and Enterprise Stack)
--------------------------------------------------------------------------------
# OpenID4VCI: A Developer's Guide to Verifiable Credential Issuance
Welcome to the developer's guide to **OpenID for Verifiable Credential Issuance (OID4VCI)**. In the world of digital identity, if a **Verifiable Credential** is your new digital passport, then **OID4VCI** is the standardized, secure, and interoperable process for the government office (the **Issuer**) to hand it to you and place it in your digital **Wallet**.
This protocol is not just a theoretical proposal; it's a foundational component of the next generation of digital identity infrastructure. In this guide, we will move beyond abstract specifications to provide a complete walkthrough of the protocol, from its core concepts and technical flows to practical implementation examples. You will learn exactly how **OID4VCI** works, how it relates to its counterpart, [**OpenID4VP**](https://docs.walt.id/concepts/data-exchange-protocols/openid4vp.md), and how you can be used together with already existing identity platforms like Keycloak.
## What is OpenID4VCI? (The Simple Explanation)
**OpenID for Verifiable Credential Issuance (OID4VCI)** is an open standard that defines a secure API for issuing Verifiable Credentials ([VCs](https://docs.walt.id/concepts/digital-credentials.md)). The protocol is built on top of the widely adopted and developer-friendly patterns of **OAuth 2.0** and **OpenID Connect**. This design choice significantly lowers the barrier to entry for developers, allowing existing identity systems to be extended to support credential issuance.
However, the true significance of **OID4VCI** lies in its role as **core infrastructure for digital identity initiatives** worldwide. In Europe, [eIDAS 2.0](https://walt.id/eidas2) mandates that by 2026 every member state must offer a European Digital Identity Wallet (EUDI Wallet). To make this ecosystem work, credential issuance must be standardized, and the EU’s Architecture and Reference Framework (ARF) explicitly names OID4VCI as the required protocol.
Globally, momentum is similar. The U.S. is piloting mobile driver’s licenses, while South Korea, Japan, and Australia align digital ID programs with global standards. Canada and several Latin American countries are testing digital credentials. Across regions, **OID4VCI is emerging as the common language** enabling verifiable, privacy-preserving credentials to flow into wallets everywhere.
Learning **OID4VCI** is not just about understanding a new protocol; it’s about equipping yourself with the technical mechanism that governments, enterprises, and ecosystems worldwide are aligning on to make digital identity practical, interoperable, and trustworthy.
## Core Concepts: The Actors and Components
To understand **OID4VCI**, you first need to understand the key players and the components they exchange. The protocol defines a clear set of roles and data structures that enable a secure and interoperable ecosystem.
### Issuer, Holder, Wallet, and Verifier
The **OID4VCI** ecosystem is composed of four primary actors:
- **Issuer**: This is the organization that creates, cryptographically signs, and issues a **Verifiable Credential**. In the context of **OID4VCI**, the **Issuer** acts as an OAuth 2.0 Resource Server and may also serve as an Authorization Server, managing the authorization process for issuance. Examples include a university issuing a digital diploma or a government agency issuing a digital driver's license.
- **Holder**: This is the entity that receives and maintains control over a credential, typically through a digital **Wallet**. In most cases, the **Holder** is the end-user or subject to whom the claims in the credential apply (e.g., a student holding their diploma). However, the **Holder** may also act on behalf of the subject — for example, a parent holding a child’s credential or an organization holding credentials about its employees.
- **Wallet**: This is the software, typically a mobile application, that the **Holder** uses to request, receive, store, and later present their credentials. From a technical perspective, during the **OID4VCI** flow, the Wallet acts as an OAuth 2.0 client application.
- **Verifier**: This is an organization that requests proof of a credential from the **Holder** in order to grant access to a service or resource. For example, a car rental agency acts as a **Verifier** when it asks to see a driver's license. While the **Verifier** is the primary recipient in the **OpenID4VP** (presentation) flow, defining it here is essential for understanding the full credential lifecycle.
### What is a Verifiable Credential?
A **Verifiable Credential (VC)** is a digital, tamper-evident collection of claims (or attributes) that an Issuer makes about a **Holder**. A key feature of the **OID4VCI** protocol is that it is format-agnostic, meaning it can be used to issue credentials in multiple formats. This flexibility allows it to support a wide range of use cases and technical requirements. The most common formats include:
- [**W3C Verifiable Credentials Data Model (VCDM)**](https://docs.walt.id/concepts/digital-credentials/verifiable-credentials-w3c.md): A flexible JSON-based format.
- [**SD-JWT VC**](https://docs.walt.id/concepts/digital-credentials/sd-jwt-vc.md): A format based on Selective Disclosure JWTs that enhances privacy by allowing the Holder to reveal only specific claims from a credential.
- [**ISO mdoc**](https://docs.walt.id/concepts/digital-credentials/mdoc-mdl-iso.md): The format used for mobile Driver's Licenses (mDL) as defined in the ISO/IEC 18013-5 standard.
Here is a simple example of what the claims inside a W3C Verifiable Credential might look like in JSON:
```json
{
// The contextual information required for data integrity and 1.api
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://www.w3.org/2018/credentials/examples/v1"
],
"id": "http://example.gov/credentials/3732",
// The credential's unique identifier
"type": [
"VerifiableCredential",
"UniversityDegree"
// Specifies the kind of credential being issued
],
"issuer": {
"id": "did:web:vc.transmute.world"
// The issuer's unique identifier (DID)
},
"issuanceDate": "2020-03-10T04:24:12.164Z",
// When the credential was issued
"credentialSubject": {
// Information about the credential's recipient and the degree earned
"id": "did:example:ebfeb1f712ebc6f1c276e12ec21",
"degree": {
"type": "BachelorDegree",
"name": "Bachelor of Science and Arts"
}
}
}
```
### The Credential Offer & The Credential Issuer Metadata
Two final components are crucial for initiating the issuance flow:
- **Credential Issuer Metadata**: For a **Wallet** to interact with an **Issuer**, it must first know what the **Issuer**'s capabilities are. This is achieved through a standardized discovery document. The **Issuer** hosts a public JSON file at a well-known location (`/.well-known/openid-credential-issuer`) that contains the **Issuer**'s metadata. This file tells the **Wallet** everything it needs to know, including the location of the **Issuer**'s `credential_endpoint` and `token_endpoint`, the types of credentials the **Issuer** supports (`credential_configurations_supported`), and the required cryptographic methods. This metadata-driven approach is the key to the protocol's scalability, as it allows any compliant **Wallet** to interact with any compliant **Issuer** without prior manual configuration.
- **Credential Offer**: This is an invitation from the **Issuer** to the **Wallet** to begin the issuance process. It can be delivered as a complete JSON object or, more commonly, as a URL (`credential_offer_uri`) that points to the object. This offer is often embedded in a QR code or a deep link. The offer contains the `credential_issuer`'s URL, which the **Wallet** uses to fetch the metadata, and a list of `credential_configuration_ids` that specify which credentials are being offered.
### Issuance Flow Variations
- **Issuer-initiated**: The Credential Issuer starts the issuance by creating and communicating a Credential Offer to the wallet (for example via a QR code or deep link). The wallet then sends the issuance-related requests in response to that offer.
- **Wallet-initiated**: The wallet starts the issuance by sending requests to the issuer without first receiving a Credential Offer. Instead, the wallet relies on issuer metadata or prior knowledge of the issuer and starts the OAuth/issuance flow directly.
## The Complete OpenID4VCI Flow Explained (With Diagrams)
The **OID4VCI** protocol defines a precise sequence of interactions between the **Wallet** and the **Issuer**. The process is designed to be both secure and flexible, accommodating different use cases through two primary authorization flows.
**Pre-Auth Flow**

### Step 1: The Credential Offer (QR Code, Deep Link)
The journey begins when you, the **Holder**, use your **Wallet** to interact with a **Credential Offer**. Most commonly, this involves scanning a QR code on an **Issuer**'s website (e.g., a university portal after graduation) or clicking a deep link sent to you via email. This offer acts as the inital step, providing your **Wallet** with the initial information it needs to find the **Issuer** and begin the process.
### Step 2: The Authorization Flow (Authorization Code vs. Pre-Authorized Code)
After processing the offer and fetching the **Issuer**'s metadata, the **Wallet** must get authorization to receive the credential. **OID4VCI** provides two distinct flows for this purpose, and the choice depends on whether your identity is already known to the Issuer at the time of the offer.
- **Authorization Code Flow**: This is the standard, browser-based login flow familiar to anyone who has used "Sign in with Google." It is used when the Credential Offer is generic or public, and the **Issuer** needs to verify your identity before issuing a personalized credential. Your **Wallet** will redirect you to the **Issuer**'s authorization server, where you authenticate (e.g., with a username and password). Upon success, the server sends an authorization code back to the **Wallet**.
- **Pre-Authorized Code Flow**: This flow is used when the **Issuer** has already authenticated you and knows who you are. For instance, you might be logged into your secure online banking portal and click a button to "Get Digital Account Statement." In this case, the **Issuer** generates a secure, single-use pre-authorized code and embeds it directly into the Credential Offer. Your Wallet receives this code and can proceed directly to the next step without requiring you to log in again.
### The Token Request & The Proof of Possession (c_nonce and JWT)
With either an authorization code or a pre-authorized code in hand, your **Wallet** makes a direct, back-channel POST request to the **Issuer**'s Token Endpoint. The goal is to exchange the code for an **Access Token**, which serves as the key to unlock the final credential.
This step includes a critical security mechanism called **Proof of Possession**. The issued credential must be cryptographically bound to a key that only your **Wallet** controls.
To prove this, the **Wallet** first obtains a fresh `c_nonce` from the **Issuer**’s Nonce Endpoint. It then creates a signed JSON Web Token (JWT) `proof` that includes the **Issuer**’s identifier as `aud` and the `c_nonce` in the nonce claim, signing it with the **Wallet**’s private key. This `proof` is then sent in the Credential Request (the final step) so the Issuer can verify the **Wallet** controls the key to which the credential will be bound, preventing interception and replay. (Earlier drafts of OID4VCI provided the `c_nonce` via the Token Endpoint, but the current specification uses the Nonce Endpoint and/or Credential Response for this purpose)
### Step 4: The Credential Request & Issuance
The final step is the credential request itself. The **Wallet** makes a POST request to the **Issuer**'s **Credential Endpoint**. This request includes two vital pieces of information:
1. The **Access Token** obtained in Step 3, which authorizes the request.
2. The signed **JWT** `proof` containing the `c_nonce`, which proves possession of the key.
The **Issuer** validates the access token and the proof. If everything is correct, it constructs the **Verifiable Credential**, signs it with its own private key, and returns it in the response body. Your **Wallet** receives the credential and stores it securely, ready for you to use.
## Practical Implementation: A Hands-On Walkthrough
Theory is essential, but code makes it concrete. Let's walk through how you can interact with an **OID4VCI Issuer** using practical examples. You can use a dedicated tool like walt.id's Issuer to see how these offers are managed in a real-world environment.
### Step 1: View Live Issuer Metadata
Before a **Wallet** can do anything, it must fetch and parse the **Issuer**'s metadata. This is a simple HTTP GET request to the `/.well-known/openid-credential-issuer` endpoint.
Here is an example of what that metadata file looks like, with comments explaining the key fields:
```json
{
// REQUIRED — The Credential Issuer's identifier.
// MUST be the same origin used to fetch this metadata (/.well-known/openid-credential-issuer).
"credential_issuer": "https://issuer.demo.walt.id/draft13",
// OPTIONAL — List of OAuth 2.0 Authorization Server identifiers the issuer relies on.
"authorization_servers": [
"https://issuer.demo.walt.id/draft13"
],
// REQUIRED — HTTPS URL of the Credential Endpoint where the Wallet requests credentials.
"credential_endpoint": "https://issuer.demo.walt.id/draft13/credential",
// OPTIONAL — HTTPS URL of the Deferred Credential Endpoint to poll for later-ready credentials.
"deferred_credential_endpoint": "https://issuer.demo.walt.id/draft13/credential_deferred",
// OPTIONAL — HTTPS URL of a Notification Endpoint the Wallet can call to report status events.
"notification_endpoint": "https://issuer.demo.walt.id/draft13/notification",
// OPTIONAL — Server capabilities for encrypting the Credential Request (application-layer encryption on top of TLS).
"credential_request_encryption": {
// REQUIRED — JWK Set containing public keys the Wallet uses for key agreement/encryption of the request.
"jwks": {
"keys": [
{
"kty": "EC",
"crv": "P-256",
"x": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"y": "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
"kid": "req-key-1",
"use": "enc"
}
]
},
// REQUIRED — Supported JWE 'enc' values the endpoint can decrypt for requests.
"enc_values_supported": ["A256GCM"],
// OPTIONAL — Supported JWE 'zip' compression values for requests.
"zip_values_supported": ["DEF"],
// REQUIRED — Whether application-layer encryption of requests is required.
"encryption_required": false
},
// OPTIONAL — Server capabilities for encrypting the Credential Response (on top of TLS).
"credential_response_encryption": {
// REQUIRED — Supported JWE 'alg' values for encrypting the response JWT.
"alg_values_supported": ["ECDH-ES"],
// REQUIRED — Supported JWE 'enc' values for encrypting the response JWT.
"enc_values_supported": ["A256GCM"],
// OPTIONAL — Supported JWE 'zip' compression values for responses.
"zip_values_supported": ["DEF"],
// REQUIRED — Whether encryption of responses is required. If true, Wallet MUST supply keys in the request.
"encryption_required": false
},
// OPTIONAL — Indicates support for issuing multiple credentials in one call to the same Credential Endpoint.
// The presence of this object means multiple proofs can be supplied, and multiple credentials can be returned.
"batch_credential_issuance": {
// REQUIRED — Maximum array size for the 'proofs' parameter in a Credential Request. MUST be >= 2.
"batch_size": 5
},
// OPTIONAL — Human-readable branding for the issuer, possibly localized.
"display": [
{
// OPTIONAL — Friendly display name of the issuer.
"name": "walt.id Demo Issuer",
// OPTIONAL — BCP47 language tag for this display object. One object per locale.
"locale": "en",
// OPTIONAL — Issuer logo metadata.
"logo": {
// REQUIRED in logo object — URI to fetch the logo (https:, data:, etc.). (
"uri": "https://issuer.demo.walt.id/assets/logo.png",
// OPTIONAL — Alt text for the logo.
"alt_text": "walt.id demo logo"
}
}
],
// REQUIRED — Map describing each credential this issuer can issue.
// Keys are configuration IDs that also appear in Credential Offers and Authorization Details.
"credential_configurations_supported": {
// Example configuration ID for a JWT-VC (non-JSON-LD) credential.
"BankId_jwt_vc_json": {
// REQUIRED — Credential format identifier. For JWT VC (non-JSON-LD) use "jwt_vc_json".
"format": "jwt_vc_json",
// OPTIONAL — Scope value usable in the Authorization Request when requesting this credential.
"scope": "BankId",
// OPTIONAL — Algorithms the issuer uses to sign this credential format (JWS alg names).
"credential_signing_alg_values_supported": ["EdDSA", "ES256", "ES256K", "RS256"],
// OPTIONAL — How the issued credential is bound to Holder-controlled key material.
// MUST be present when key binding is required; omit if not required.
// Use 'jwk', 'cose_key', or DID method tags like 'did:web', 'did:key', etc.
"cryptographic_binding_methods_supported": ["did:web", "jwk"],
// OPTIONAL (and REQUIRED if cryptographic_binding_methods_supported is present) —
// Proof type(s) the issuer accepts at the Credential Endpoint; Wallet references one of these in the request.
"proof_types_supported": {
// 'jwt' proof type: Wallet signs a JWT including 'aud' (issuer) and 'nonce' (c_nonce).
"jwt": {
// REQUIRED within proof type — Accepted signing algs for the proof JWT.
"proof_signing_alg_values_supported": ["EdDSA", "ES256", "ES256K", "RS256"]
}
},
// JWT VC (non-JSON-LD) format-specific parameter: credential_definition with VC types.
"credential_definition": {
// REQUIRED — VC type array as per VCDM; commonly includes "VerifiableCredential" and the specific type.
"type": ["VerifiableCredential", "BankId"]
},
// OPTIONAL — Hints for Wallet UI on how to present claims and the credential card.
"credential_metadata": {
// OPTIONAL — Claim display metadata with JSON-path-like 'path' pointers.
"claims": [
{ "path": ["credentialSubject", "given_name"] },
{ "path": ["credentialSubject", "family_name"] },
{ "path": ["credentialSubject", "account_number"] }
],
// OPTIONAL — Overall card display hints.
"display": [
{
"name": "Bank Identity",
"locale": "en",
"logo": {
"uri": "https://issuer.demo.walt.id/assets/bankid.png",
"alt_text": "Bank ID"
},
"background_color": "#12107c",
"text_color": "#FFFFFF"
}
]
}
}
}
}
```
[Explore Example Metadata](https://issuer.demo.walt.id/swagger/index.html#/oidc/get__standardVersion___well_known_openid_credential_issuer)
### Step 2: Create a Credential Offer via a UI
For many use cases, generating a Credential Offer doesn't require writing code. An administrator can simply use a web portal, like the walt.id Web Issuer Portal to configure the credential and it's data, and the system will generate the corresponding QR code or offer link.
Try the Issuer Portal [here](https://portal.demo.walt.id/)
### Step 3: Create a Credential Offer Programmatically via API
For automated workflows you can trigger the process directly via an API call. The open-source **Issuer** API from walt.id for example will handle the subsequent exchanges with the **Wallet** to deliver the final credential.
You would typically make a POST request like the one below to create a credential offer. Execute the call in your termninal to create a credential offer for a Verifiable University Degree. You can claim it afterwards using the walt.id **Wallet** [here](https://wallet.demo.walt.id/).
```curl
curl -X 'POST' \
'https://issuer.demo.walt.id/openid4vc/jwt/issue' \
-H 'accept: text/plain' \
-H 'statusCallbackUri: https://example.com/$id' \
-H 'Content-Type: application/json' \
-d '{
"issuerKey": {
"type": "jwk",
"jwk": {
"kty": "OKP",
"d": "JvJIpga2GD8LJeRu4Sv-mL4thE31DuFlr9PA04CIoZY",
"crv": "Ed25519",
"kid": "iJMS5bkZVIlncfq_Lf_SuxJ2JtQ5Hvaz7tWPnAjUUds",
"x": "FZdvwC8aGhRwqzWptej0NZgtwYAI1SyFg1mKDETOfqE"
}
},
"issuerDid": "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoiaUpNUzVia1pWSWxuY2ZxX0xmX1N1eEoySnRRNUh2YXo3dFdQbkFqVVVkcyIsIngiOiJGWmR2d0M4YUdoUndxeldwdGVqME5aZ3R3WUFJMVN5RmcxbUtERVRPZnFFIn0",
"credentialConfigurationId": "UniversityDegree_jwt_vc_json",
"credentialData": {
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://www.w3.org/2018/credentials/examples/v1"
],
"id": "http://example.gov/credentials/3732",
"type": [
"VerifiableCredential",
"UniversityDegree"
],
"issuer": {
"id": "did:web:vc.transmute.world"
},
"issuanceDate": "2020-03-10T04:24:12.164Z",
"credentialSubject": {
"id": "did:example:ebfeb1f712ebc6f1c276e12ec21",
"degree": {
"type": "BachelorDegree",
"name": "Bachelor of Science and Arts"
}
}
},
"mapping": {
"id": "",
"issuer": {
"id": ""
},
"credentialSubject": {
"id": ""
},
"issuanceDate": "",
"expirationDate": ""
},
"authenticationMethod": "PRE_AUTHORIZED",
"standardVersion": "DRAFT13"
}'
```
The API would respond with the Credential Offer, which includes the URI that you can then convert into a QR code or send to the user:
```
openid-credential-offer://issuer.portal.walt.id/?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fissuer.portal.walt.id%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22jwt_vc_json%22%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegree%22%5D%2C%22credential_definition%22%3A%7B%22%40context%22%3A%5B%22https%3A%2F%2Fwww.w3.org%2F2018%2Fcredentials%2Fv1%22%2C%22https%3A%2F%2Fwww.w3.org%2F2018%2Fcredentials%2Fexamples%2Fv1%22%5D%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegree%22%5D%7D%7D%5D%2C%22grants%22%3A%7B%22authorization_code%22%3A%7B%22issuer_state%22%3A%220431b78c-cd94-4f50-bfdf-e24d436c0cf6%22%7D%2C%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiIwNDMxYjc4Yy1jZDk0LTRmNTAtYmZkZi1lMjRkNDM2YzBjZjYiLCJpc3MiOiJodHRwczovL2lzc3Vlci5wb3J0YWwud2FsdC5pZCIsImF1ZCI6IlRPS0VOIn0.NorG7GtjmA-HXMJfUzU9vfnshcIgFY0oYQb8qJjDfORPoNxuurgySSOIDKFi7Z4XJmC-oJZnM0Nbb0NUd57cDA%22%2C%22user_pin_required%22%3Afalse%7D%7D%7D
```
Follow the full API Guide [here](https://docs.walt.id/community-stack/issuer/credential-issuance/vc-oid4vc.md).
## OpenID4VCI vs. OpenID4VP: The Full Lifecycle
A common point of confusion for developers new to this space is the relationship between **OID4VCI** and **OpenID4VP**. The distinction is simple yet fundamental:
- **OpenID4VCI** is for **Issuance**: It defines the protocol for an Issuer to securely give you a credential.
- **OpenID4VP** (Verifiable Presentations) is for **Presentation**: It defines the protocol for you to securely show that credential to a **Verifier**.
These are two separate but complementary specifications that fall under the broader **OpenID for Verifiable Credentials (OID4VC)** umbrella. Together, they cover the entire lifecycle of a credential. The architectural separation is a deliberate design choice that enhances privacy and security. By separating the issuance and presentation flows, the original **Issuer** is not involved in the day-to-day use of the credential. This means the **Issuer** cannot track where or when you present your credential, a core principle of decentralized, user-centric identity.
## Integrating OpenID4VCI with Keycloak
One of the most powerful features of **OID4VCI** is its ability to integrate with existing, standard-compliant Identity Providers (IdPs). Because the protocol leverages the standard OAuth 2.0 **Authorization Code Flow**, you can use an IdP like **Keycloak** to handle user authentication and provide the claims for a credential.
This allows you to issue credentials based on data that is verified in real-time by your existing, trusted identity system. The process works as follows:
1. A user scans a QR code, and their **Wallet** initiates the issuance flow with your **OID4VCI** service.
2. Your service, acting as an OAuth 2.0 client to **Keycloak**, redirects the user to your organization's **Keycloak** login page.
3. The user authenticates successfully using their existing corporate or customer credentials.
4. **Keycloak** returns an authorization token to your OID4VCI service.
5. Your service uses this token to securely call **Keycloak**'s `/userinfo` endpoint and fetch verified claims about the user (e.g., name, email, role, user ID).
6. This trusted data is then mapped into the attributes of the **Verifiable Credential**, which is then signed and issued to the user's **Wallet**.
This method is perfect for enterprise or customer-facing scenarios where user data must be pulled from a source of truth like **Keycloak**, rather than being provided manually.
[walt.id's Issuer API](https://docs.walt.id/community-stack/issuer/getting-started.md) service can therefore be configured to use
Keycloak or any other OIDC compliant Identity Provider to authenticate users and populate **Verifiable Credentials** with verified user claims.
## Understanding OID4VCI Versions (Draft 11 vs. Draft 13 vs. OID4VCI 1.0)
The **OID4VCI** specification has evolved through multiple drafts to OID4VCI 1.0. The table below summarizes the key changes across Draft 11, Draft 13, and OID4VCI 1.0.
| Area | Draft 11 | Draft 13 | OID4VCI 1.0 |
|------|----------|----------|------------|
| Credential Offer – key parameter name | credentials (array; strings and, in earlier drafts, objects seen in examples) | Renamed to credential_configuration _ids (array of string identifiers) | Kept as credential_configuration _ids |
| Issuer metadata – list of credential options | credentials_supported (structure evolved across early drafts) | Renamed to credential_configurations _supported; structure changed from array → map | Same (credential _configurations _supported) |
| Pre-Authorized Code flow – end-user code | Parameter user_pin_required | Replaced by a structured tx_code object (with description, length, input_mode, etc.) | tx_code retained |
| Credential Offer – by value vs by reference | credential_offer / credential_offer_uri defined | Same, clarified; examples updated and statement that offer cannot be signed | Same behavior and discovery model |
| Authorization request expression | authorization_details type openid_credential (and scope) | Adds option to reference by credential_configuration_id (not only format/type) | Same; plus end-to-end identifiers guidance |
| Token response identifiers | — | Option to return authorization_details with credential_identifiers to correlate in later steps | Same; referenced in flow summary |
| Request/response encryption parameters | Separate fields (credential_encryption _jwk, credential_response _encryption_alg, …_enc) | Grouped into credential_response _encryption object | v1 also defines request encryption via credential_request _encryption in Issuer metadata |
| Authorization server discovery list | authorization_servers singular (earlier text) | Now an array | Remains array |
| Batch issuance mechanics | Dedicated Batch Credential Endpoint (§ 8) | Still present as an optional endpoint | Removed as a separate endpoint; batching happens at the Credential Endpoint for multiple creds with the same format & dataset in one request |
| Nonce handling for proofs | c_nonce discussed within credential flow | Clarifications | New dedicated Nonce Endpoint (optional) to fetch fresh c_nonce; requirements for including c_nonce in proofs |
| Notification / status callback | — | — | New optional Notification mechanism: Issuer may receive notifications about status of issued credential(s) |
Implementations like walt.id's Issuer support multiple draft versions and OID4VCI 1.0 coming in Q4 2025.
## Next steps
- [OID4VP](https://docs.walt.id/concepts/data-exchange-protocols/openid4vp.md) - Explore the protocol Verifiers use in more detail.
- [How to issue a Verifiable Credential via OID4VCI](https://docs.walt.id/community-stack/issuer/credential-issuance/vc-oid4vc.md) - Checkout the walt.id Issuer API docs for issuing a VC via OID4VCI, trusted by :traction-devs{} developers and organizations worldwide.
--------------------------------------------------------------------------------
PAGE: What is OpenID4VP? The Developer's Guide (2026)
URL: https://docs.walt.id/concepts/data-exchange-protocols/openid4vp
RAW: https://docs.walt.id/concepts/data-exchange-protocols/openid4vp.md
STACK: Concepts (stack-agnostic, applies to both Community and Enterprise Stack)
--------------------------------------------------------------------------------
# OpenID4VP: A Developer's Guide to Verifiable Presentations
In today's digital world, proving who you are online is often a clumsy and risky process. We're asked to share entire identity documents just to prove a single fact, like our age, leading to oversharing of sensitive personal data. The current landscape relies on centralized identity providers, creating data silos and giving users little control over their own information. This model is not only inefficient but also ripe for phishing and fraud. The solution to this problem is a standardized, decentralized approach to identity verification.
**OpenID for Verifiable Presentations (OpenID4VP)** is a protocol built on the foundations of **OAuth 2.0** that standardizes how a person (a **Holder**) can present cryptographically secure digital credentials to an online service (a **Verifier**) in a secure, privacy-preserving, and user-controlled way. It creates a common language for requesting and receiving proof of identity attributes, putting you in control via your digital **Wallet** and enabling the crucial principle of data minimization.
In essence, **OpenID4VP** is the technical framework for a digital "show your ID" online, but with the power to show only the specific pieces of information required for a given transaction.
## What is OpenID4VP, Exactly?
To fully grasp the protocol, it's helpful to look at it from a few different angles—from a simple real-world analogy to its formal definition and the key players involved in every interaction.
### In Simple Terms: A Digital "Show Your ID" for the Internet
Imagine you're at a concert venue, and a security guard needs to verify that you are old enough to enter. You pull out your driver's license. The guard looks at your photo to see it's you and checks your date of birth to confirm you're over 21. Critically, they don't need to know your home address or any other attribute on the credential. They verify the one piece of information they need and let you pass. OpenID4VP is the technical framework that enables this exact, privacy-preserving interaction on the internet. It allows a website (**Verifier**) to ask your digital Wallet for proof of a specific claim (like being over 21), and for your wallet to provide just that proof without revealing your entire digital identity.
### The Official Definition Explained
Formally, **OpenID4VP** is a protocol that extends standards like **OAuth 2.0** and **OpenID Connect**, enabling users to present **Verifiable Presentations (VPs)**—which are packages containing one or more [**Verifiable Credentials (VCs)**](https://docs.walt.id/concepts/digital-credentials.md)—to prove claims about themselves.
Let's break down the key components of this definition:
- **Extends OAuth 2.0**: The protocol is built upon the widly used authorization flows of **OAuth 2.0.** This is a significant advantage for developers, as it uses a familiar security architecture, reducing the learning curve and making it easier to integrate into existing systems.
- **Verifiable Credentials (VCs)**: These are the digital equivalents of your physical credentials like a driver's license, a university diploma, or an event ticket. A VC is a set of claims about a subject, issued by a trusted entity (like a government or university), and is cryptographically signed to be tamper-evident. These VCs are stored securely in your digital **wallet**.
- **Verifiable Presentations (VPs)**: This is a data package created by your wallet at the moment of presentation. It contains the specific VCs (or just parts of them) that a **Verifier** has requested. The VP is then digitally signed by you, the Holder, which serves as cryptographic proof of your consent and control over the data being shared in that specific transaction.
### Key Actors: The Verifier, the Holder, and the Wallet
Every **OpenID4VP** interaction involves three distinct roles, and understanding them is fundamental to understanding the protocol's architecture.
- **The Holder**: This is you, the end-user. The **Holder** is the individual who possesses the digital credentials and has ultimate control over their own identity data. You are the subject of the claims being asserted in the VCs.
- **The Verifier (or Relying Party)**: This is the application, website, or service that needs to verify some piece of information about the **Holder**. A **Verifier** could be an e-commerce site checking your age, a bank performing a Know Your Customer (KYC) check, or a venue scanning your digital ticket.
- **The Wallet**: This is the software, typically a mobile application, that you use to securely store your VCs, receive verification requests, and manage the presentation process. The **Wallet** acts as your digital agent, working on your behalf to interpret requests, get your explicit consent, and construct the response for the **Verifier**.
This three-party model represents a fundamental architectural shift away from traditional, federated identity systems. In a typical "Sign in with Google" flow, the interaction is primarily between you and the Identity Provider (IdP); the service you're accessing simply trusts the IdP. The verifiable credentials model decouples these roles. An **Issuer** creates the credential, the **Holder** stores it, and the **Verifier** consumes it, often without the **Issuer** being directly involved in the verification transaction at all. This decoupling is the core change. A **Verifier** doesn't need a pre-existing technical integration with every possible **Issuer**. Instead, it only needs to be able to verify the cryptographic proof of the credential presented by the **Holder**, creating a more open, interoperable, and scalable "web of trust".
## How the OpenID4VP Protocol Works (A Step-by-Step Flow)
The OpenID4VP protocol defines a clear, secure sequence of interactions between the **Verifier**, the **Holder**, and the **Wallet**. While the underlying cryptography is complex, the flow itself is logical and designed for a seamless user experience, whether on a single device or across multiple devices (like a laptop and a phone).
The following diagram illustrates the standard cross-device flow, which is the most common implementation.

### Step 1: The Verifier Creates a Presentation Request
The process always begins with the Verifier needing to confirm one or more claims about a user. To do this, it constructs an **Authorization Request**. At the heart of this request is a `presentation_definition` or, in the latest specification, a `dcql_query`, which precisely defines what credentials and claims are needed (e.g., "a government-issued ID that contains a date_of_birth claim"). This request is then encoded into a URL, which is typically presented to the user as a QR code for cross-device interactions or a deep link for same-device flows
### Step 2: The Holder's Wallet Presents the Credentials
The **Holder** initiates the interaction by scanning the QR code with their **Wallet** application. The **Wallet** then takes over as the user's agent:
1. It decodes the URL from the QR code and uses the information to fetch the full **Authorization Request** object from the **Verifier**.
2. It parses the request to understand the **Verifier**'s requirements.
3. The **Wallet** searches its secure storage for any VCs that satisfy the request.
4. Crucially, it prompts the **Holder** for explicit consent. The user is shown exactly what information is being requested and who is requesting it (e.g., "OnlineStore.com would like to verify you are over 18."). No data is shared without this consent.
5. Once the user approves, the **Wallet** prepars the presentation. Depending on the credential format:
- [W3C VC](https://docs.walt.id/concepts/digital-credentials/verifiable-credentials-w3c.md): Create a Verifiable Presentation (JWT-VP or Data-Integrity VP) controlled and signed by the Holder.
- [SD-JWT VC](https://docs.walt.id/concepts/digital-credentials/sd-jwt-vc.md): Return the SD-JWT with the selected Disclosures and, if holder binding is required, add a Key Binding JWT.
- ISO [mDL/mdoc](https://docs.walt.id/concepts/digital-credentials/mdoc-mdl-iso.md) (ISO/IEC 18013-7): Return a DeviceResponse, bound to the Verifier’s request through the SessionTranscript and OID4VP hand-over structure.
6. It packages the result into a `vp_token` (and, if applicable, a `presentation_submission`) and send it to the **Verifier**’s HTTPS endpoint using **OID4VP**’s `response_mode=direct_post`.
### Step 3: The Verifier Receives and Validates the Presentation
The **Wallet**’s Authorization Response arrives at the **Verifier**’s HTTPS endpoint via **OID4VP**’s `response_mode=direct_post`. It includes a `vp_token` (and sometimes a `presentation_submission`), whose content depends on the credential format.
The **Verifier** then validates:
1. Holder binding & replay protection: Confirm that nonces and audience values match the request, ensuring the response is fresh and bound to this Verifier.
2. Issuer authenticity: Verify the Issuer’s signatures — on credentials (W3C), the SD-JWT, or mdoc data elements. Resolve trust chains (e.g., via [DIDs](https://docs.walt.id/concepts/decentralised-identifiers.md)) where required.
3. Selective disclosure (if applicable): For SD-JWTs, recompute disclosed values against the digests.
4. Claim satisfaction: Ensure the presented data fulfills the **Verifier**’s original request.
5. Status & validity: Check that credentials or documents have not been revoked, suspended, or expired.
6. Format-specific checks:
- W3C VC: Validate the Verifiable Presentation (JWT-VP or Data-Integrity VP) structure.
- SD-JWT VC: Validate the Key Binding JWT when holder binding is required.
- ISO mDL/mdoc: Validate the SessionTranscript and OID4VP hand-over structure for integrity and replay protection.
If all checks succeed, the **Verifier** can trust the authenticity, integrity, and freshness of the claims and proceed with the transaction (e.g., granting access or completing a purchase).
## OpenID4VP vs. OpenID4VCI: What's the Difference?
Within the decentralized identity ecosystem, you will frequently encounter two related acronyms: **OpenID4VP** and [**OpenID4VCI**](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md). While they sound similar and are part of the same family of specifications, they serve two distinct and complementary purposes: presentation and issuance. Understanding the difference is key to understanding the full lifecycle of a verifiable credential.
### OpenID4VCI (Issuance): Minting the Digital Credential
[**OpenID for Verifiable Credential Issuance (OpenID4VCI)**](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md) is the protocol used to get a new credential into your wallet in the first place. The primary interaction in this flow is between an **Issuer** (e.g., a university, government agency, or employer) and the **Holder**'s Wallet. The **Issuer** uses the [**OID4VCI**](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md) protocol to securely offer and deliver a newly created VC to the **Holder** after they have authenticated and proven their eligibility. You can think of this as the "enrollment" or "credential pickup" phase.
### OpenID4VP (Presentation): Showing the Digital Credential
As we've discussed, **OpenID4VP** is the protocol for using a credential that is already stored in your wallet. The primary interaction here is between the **Holder**'s **Wallet** and a **Verifier**. The **Holder** uses **OpenID4VP** to present proof of claims from their existing credentials to a third party that needs to verify them. This is the "verification" or "show your ID" phase.
### How They Work Together in a Decentralized Identity Ecosystem
**OID4VCI** and **OID4VP** are not competing standards; they are two sides of the same coin, designed to work together to enable a complete, end-to-end credential lifecycle. A credential has no value if it cannot be both reliably issued and reliably presented.
Consider this complete workflow:
1. [**Issuance (OID4VCI)**](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md): You graduate from a university (**Issuer**). You log into the university's alumni portal and request a digital version of your diploma. The university uses [**OpenID4VCI**](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md) to issue a `VerifiableDiploma` credential directly and securely to your digital **Wallet**.
2. **Presentation (OpenID4VP)**: Months later, you apply for a job at a new company (**Verifier**). The company's online application portal requires proof of your degree. The portal generates a request using **OpenID4VP**. You scan a QR code, and your **Wallet** uses **OpenID4VP** to present your `VerifiableDiploma` to the company for instant, cryptographic verification.
These two protocols are the fundamental pillars of the broader **OpenID for Verifiable Credentials (OID4VC)** suite, which aims to standardize the entire lifecycle of a digital credential. The combined adoption of both [**OID4VCI**](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md) and **OID4VP** by major initiatives like the EU Digital Identity Wallet demonstrates that a mature, interoperable ecosystem requires standardized solutions for both getting credentials and using them.
| Feature | OpenID4VCI (Issuance) | OpenID4VP (Presentation) |
|--------------------------|----------------------------------------------------|---------------------------------------------------|
| **Primary Goal** | To securely issue a new VC to a wallet. | To securely present an existing VC from a wallet. |
| **Primary Actors** | Issuer ↔ Holder's Wallet | Verifier ↔ Holder's Wallet |
| **Direction of Credential** | Issuer → Holder | Holder → Verifier |
| **Analogy** | Receiving your driver's license at the DMV. | Showing your driver's license to a bouncer. |
| **Core Protocol Action** | Credential Offer & Issuance Request | Presentation Request & Authorization Response |
## Understanding the Specification: From Drafts to DCQL
Like many open standards, the **OpenID4VP** specification has evolved over time through a collaborative, public process. This has led to some confusion among developers regarding different draft versions and, most importantly, a significant technical upgrade in how credential requests are made.
### Why Are There So Many Drafts? A Look at the OIDF Process
Standards developed at the OpenID Foundation (OIDF) go through a rigorous, iterative process before being finalized. They are published as a series of numbered "implementer's drafts," which allow the global community of developers, companies, and security experts to build prototypes, provide feedback, and refine the protocol based on real-world testing. This open process is crucial for ensuring that the final `v1.0` standard is robust, secure, and, most importantly, interoperable across different wallets and verifier systems, as proven in recent successful interoperability events.
### The Big Change: From Presentation Definition to DCQL
Early drafts of the **OpenID4VP** specification relied on a mechanism called Presentation Definition to allow **Verifiers** to specify their credential requirements. This mechanism, which comes from the Decentralized Identity Foundation's (DIF) Presentation Exchange specification, is a declarative JSON format that lets a **Verifier** describe the credentials it is looking for.
While functional for simple requests, the latest versions of the **OpenID4VP** specification (starting with Draft 22 in October 2024) have introduced a more powerful and expressive mechanism: the **Digital Credentials Query Language ([DCQL](https://docs.walt.id/concepts/data-exchange-protocols/dcql.md))**. The final, stable `v1.0` version of the specification includes **DCQL** as a core, standardized component.
### What is DCQL (Decentralized Query Language)?
**DCQL** is a JSON-encoded query language designed specifically for requesting **Verifiable Presentations**. It allows a **Verifier** to construct precise, structured queries to request credentials that match specific and even complex criteria.
DCQL offers several advantages over the older **Presentation Definition** approach:
- **Greater Expressiveness**: DCQL can formulate much more complex and logical queries. For example, a **Verifier** can use **DCQL** to ask for "a `VerifiableID` where the `country` is 'US' AND an `EmployeeCredential` where the role is 'Engineer'", all within a single, structured request.
- **Standardization**: **DCQL** provides a single, native query language defined within the **OpenID4VP** specification itself.
- **Future-Proofing**: DCQL is already being mandated as the required query language in high-assurance industry profiles.
This evolution from **Presentation Definition** to **DCQL** signals a critical maturation of the protocol. It represents a shift from simple "credential matching" to sophisticated "verifiable data querying." Early use cases were straightforward: "Do you have a credential of type X?" As the ecosystem and its use cases have grown more complex, the need for a true query language became apparent. **DCQL** enables a **Verifier** to treat a user's wallet less like a simple folder of documents and more like a private, queryable, user-controlled database.
## What is a Verifiable Presentation (VP)?
In **OpenID4VP**, the response is a `vp_token` —a container that can hold W3C Verifiable Presentations and/or other presentation types depending on the credential format. The spec states that a VP Token “contains one or more Verifiable Presentations and/or Presentations … in the same or different Credential formats,” and supports W3C VCDM, ISO mdoc, and IETF SD-JWT VC. In other words: a VP is central when you use the W3C VC Data Model, but other formats use their own presentation artefacts.
### The Core Components of a W3C Verifiable Presentation (VP)
A W3C **Verifiable Presentation** is a data structure that **packages data from one or more Verifiable Credentials** so the authorship is verifiable. Common fields include:
- `@context`: Specifies the JSON-LD context, which defines the vocabulary and semantics of the data model (e.g., the W3C Verifiable Credentials v1 standard).
- `type`: An array that declares the object's type, which must include `VerifiablePresentation`.
- `verifiableCredential`: An array containing one or more VCs. These can be the full, original VCs or, in the case of selective disclosure, just specific claims from them.
- `holder` - An identifier for the user (often a **Decentralized Identifier (DID)**) who is creating and presenting the VP. This identifies who is making the presentation.
For JWT-encoded VPs, the vp claim MUST be present in the JWT payload.
### How the Credential Format changes the “presentation”
#### W3C VCDM (e.g., VC-JWT / Data Integrity):
Inside the VP, verifiableCredential can hold VC-JWTs (compact JWTs) or JSON-LD credentials, and the VP is signed by the Holder (e.g., via JWS/JWT or Data Integrity proof). The Data Model details both embedded (Linked Data) and external (JWT) proofs and the required vp claim for JWT VPs.
#### SD-JWT VC (IETF):
The presentation is an SD-JWT presentation consisting of the SD-JWT plus selected Disclosures and optionally a KB-JWT for holder binding—not a W3C VP wrapper by default. The IETF draft states: “An SD-JWT consists of … the SD-JWT itself, optional disclosures, and an optional KB-JWT,” and “A presentation of an SD-JWT VC MAY contain a KB-JWT.” OpenID4VP transports this as a Presentation inside the VP Token.
#### mDL / mdoc (ISO 18013-5, 18013-7):
OpenID4VP carries the ISO DeviceResponse CBOR structure in the VP Token (again, not a W3C VP). The spec: “The VP Token contains the base64url-encoded DeviceResponse CBOR structure as defined in ISO/IEC 18013-5 ….” ISO/IEC 18013-7 adds over-the-internet presentation via OpenID4VP.
#### Example of a JWT-based Verifiable Presentation (JSON Snippet)
```json
{
// The @context specifies the data model, in this case, the W3C Verifiable Credentials v1 standard.
"@context": [
"https://www.w3.org/2018/credentials/v1"
],
// The id provides a unique identifier for this specific presentation instance.
"id": "Fs2M6HVrER7y",
// The type array declares that this object is a Verifiable Presentation.
"type": [
"VerifiablePresentation"
],
// The holder property contains the Decentralized Identifier (DID) of the user who is presenting the credential.
"holder": "did:key:z6MkwQx8J3LwFzYjv7Lqk7h2b7Zr5dF6Qe5mQ3E8iJ2o8YkP",
// This array contains the actual credential(s) being presented. It's a JWT string in this example.
"verifiableCredential": [
"eyJraWQiOiJkaWQ6andrOmV5SnJkSGtpT2lKRlF5SXNJ..."
]
}
```
## Real-World OpenID4VP Use Cases & Implementations
The true value of a technical protocol lies in its ability to solve real-world problems. **OpenID4VP** is already being deployed across various industries to create more secure, efficient, and privacy-preserving user experiences. Use-case examples include:
### Use Case 1: Streamlining KYC for Financial Services
- **The Problem**: Traditional Know Your Customer (KYC) processes for opening a bank account are notoriously slow, require manual document verification, and force users to upload highly sensitive information, creating a large attack surface for data breaches.
- **The OpenID4VP Solution**: A bank, acting as a **Verifier**, can use **OpenID4VP** to request the necessary claims (e.g., `full_name`, `date_of_birth`, `address`) from a user's government-issued digital ID (like an mDL). The user simply scans a QR code and consents to the data sharing from their **Wallet**. The bank receives a cryptographically verified and trustworthy attestation in seconds, dramatically reducing onboarding friction, minimizing fraud, and improving regulatory compliance
### Use Case 2: Age Verification for Restricted Content
- **The Problem**: Websites that sell age-restricted products like alcohol or offer services like online gambling must verify a user's age. Simply asking for a date of birth is not sufficient, but asking for a full ID scan means the user needs to share information that is not directly required (e.g. address).
- **The OpenID4VP Solution**: An e-commerce site (Verifier) can request a single, boolean claim: `is_over_18`: true. A user with a digital driver's license in their wallet can selectively disclose only this claim. They prove they meet the age requirement without revealing their name, address, or exact date of birth. This is a perfect example of data minimization in practice, enhancing user privacy while still meeting business needs.
### Use Case 3: Digital Ticketing for Events and Venues
- **The Problem**: Traditional paper tickets or simple QR codes are easy to counterfeit, scalp, or fraudulently resell, leading to revenue loss for organizers and frustration for attendees.
- **The OpenID4VP Solution**: An event organizer (**Issuer**) uses [**OID4VCI**](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md) to issue a Verifiable Credential as a ticket, which is cryptographically bound to the purchaser's identity (**Holder**). When the attendee arrives at the venue (**Verifier**), they present the ticket from their wallet using **OpenID4VP**. The scanner at the gate can instantly verify the ticket's authenticity, confirm that it has not been used before, and ensure that it is being presented by its legitimate owner, effectively eliminating ticket fraud.
## Practical Implementation: A Hands-On Walkthrough
For a developer building a service that needs to verify user data, implementing **OpenID4VP** involves creating the request and handling the response. Here is a high-level walkthrough of the **Verifier**'s role in the process.
### Step 1: Define What You Need (DCQL)
The first step for any **Verifier** is to precisely define the data it needs to request. With the finalization of the `v1.0` specification, **DCQL** is the modern, standard way to construct this query. You would create a JSON object that specifies the credential types, formats, and specific claims you require. Numerous open-source libraries and tools are emerging to help build and validate these DCQL queries programmatically.
You can for example, explore the walt.id's open-source verifier [here](https://docs.walt.id/community-stack/verifier), which let's you request and validate credentials via UI, API & SDKs.
### Step 2: Generate a Presentation Request and Present it via a UI (QR Code Flow)
This is the most common flow for web applications interacting with a user's mobile wallet.
1. Your backend service generates the full **Authorization Request**, including the DCQL query, a unique cryptographic `nonce` to prevent replay attacks, and the `response_uri` where the wallet should send the final VP.
2. Your service then creates a `request_uri` that points to this full request object.
3. Your frontend application takes this `request_uri` and embeds it into a standardized URL with the scheme `openid4vp://`. This complete URL is then rendered as a QR code on the screen for the user to scan with their wallet app.
You can experience this in action by trying out the open-source walt.id verifier portal [here](https://portal.demo.walt.id/).
### Step 3: Generate a Presentation Request via API and Send it as a Direct Link (Same-Device Flow)
In this flow, a backend service constructus the Authorization Request and sends it to the user as a link, in
for example an email.
1. Your application constructs the `openid4vp://` URL containing the `request_uri` just as in the cross-device flow.
2. Instead of rendering a QR code, your app uses a deep link to invoke the user's installed wallet app directly, passing the request parameters seamlessly. The wallet opens, prompts the user for consent, and returns the response to your application, all within a smooth, in-app workflow.
You can use the walt.id open-source Verifier API to handle the Authorization Request creation and perform various checks on presented credentials. Learn more [here](https://docs.walt.id/community-stack/verifier/credential-verification/vc-oid4vc.md).
## OpenID4VP FAQ
### What is the OpenID standard?
The **OpenID Foundation (OIDF)** is a non-profit international standards body that develops open, interoperable standards for digital identity. Its most famous standard is **OpenID Connect**, which is used by billions of people for secure logins across the web, but the **OIDF** also maintains the entire **OpenID for Verifiable Credentials (OID4VC)** family of specifications, including **OpenID4VP**.
### Is OpenID4VP the same as OpenID Connect?
No. **OpenID Connect** is an identity layer on top of **OAuth 2.0** used to authenticate the End-User and optionally return claims. **OpenID4VP** is a separate **OpenID Foundation specification** that defines a mechanism on top of **OAuth 2.0** to request and deliver presentations of credentials (often via a user’s **Wallet**). Both reuse **OAuth 2.0**, but their primary goals differ: OIDC = authentication, OID4VP = credential presentation.
### Where does OpenID4VP fit in the Self-Sovereign Identity (SSI) model?
Self-Sovereign Identity (SSI) is a conceptual model for digital identity that puts individuals in complete control of their own personal data, independent of any central authority. OpenID4VP is a crucial piece of the technical implementation that makes the SSI vision a reality. It provides the standardized, interoperable protocol that allows a user (**Holder**) to exercise their sovereign control by selectively sharing their data from their personal **Wallet** with **Verifiers**. It is a key specification that makes the principles of SSI practical, secure, and ready for widespread adoption.
## Next steps
- [OID4VCI](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md) - Explore the protocol Issuers use in more detail.
- [How to verify a Verifiable Credential via OID4VP](https://docs.walt.id/community-stack/verifier/credential-verification/vc-oid4vc.md) - Checkout the walt.id Verifier API docs for verifying a VC via OID4VP, trusted by :traction-devs{} developers and organizations worldwide.
--------------------------------------------------------------------------------
PAGE: What is the Digital Credentials API? The Developer's Guide (2026)
URL: https://docs.walt.id/concepts/data-exchange-protocols/dc-api
RAW: https://docs.walt.id/concepts/data-exchange-protocols/dc-api.md
STACK: Concepts (stack-agnostic, applies to both Community and Enterprise Stack)
--------------------------------------------------------------------------------
# W3C Digital Credentials API (DC-API): A Developer's Guide to Browser-Based Digital Credentials
In the evolving landscape of digital identity, one of the biggest gaps has been a **native, browser-based way** for users to present and receive digital credentials – without QR codes, custom URL schemes, or hard-to-debug cross-device hacks.
Protocols like **OpenID for Verifiable Credential Issuance ([OpenID4VCI](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md))** and **OpenID for Verifiable Presentations ([OpenID4VP](https://docs.walt.id/concepts/data-exchange-protocols/openid4vp.md))** standardize the issuance and presentation of verifiable credentials across wallets and services. But until recently, these flows have largely lived **outside** the browser (mobile apps, deep links, QR flows).
**The W3C Digital Credentials API (often called “DC-API” or “DC API”)** changes that. It is a web platform API that lets websites request and receive digital credentials **directly in the browser**, with the user agent mediating the interaction between the website and the user’s wallet or credential store.
**Note:**
**TL;DR:**
- **DC-API** is a W3C browser API that mediates **presentation and issuance** of digital credentials.
- **DC-API** is **protocol-agnostic** but works hand-in-hand with protocols like **OpenID4VP** and **OpenID4VCI**.
- Chrome and Safari have **shipping implementations**, with cross-device flows for mobile wallets and desktop browsers.
- **DC-API** is on the W3C **Recommendation track** and evolving quickly, so you should design for change but can already prototype in real browsers.
In essence, **Digital Credentials API is the bridge between web apps and digital wallets**: it brings verifiable credentials into everyday web experiences while preserving cryptographic security and user privacy.
## What is Digitial Credentials API (DC-API), Exactly?
### In Simple Terms: A Browser-Based “Show Your ID” Button
Imagine you’re on an e-commerce site and want to buy age-restricted goods. Today you might:
- Upload a photo of your ID,
- Or switch to a mobile app,
- Or scan a QR code with another device.
With **Digigtal Credentials API**, the website can instead ask:
> “Please prove you’re over 21.”
Your **browser**:
1. Shows you a system UI explaining what’s being requested and by which site.
2. Talks to your wallet (on the same device or another device).
3. Lets you pick a credential that satisfies the request.
4. Returns a **cryptographically verifiable presentation** back to the website.
You stay in the browser. You see clearly what’s being shared. The site gets a **verifiable proof** instead of a blurry JPG.
### The Official Definition (De-Jargonized)
Formally, the **W3C Digital Credentials API** is a JavaScript API that enables user agents (browsers) to mediate **presentation and issuance** of digital credentials (such as mobile IDs, [mDLs](https://docs.walt.id/concepts/digital-credentials/mdoc-mdl-iso.md), or verifiable credentials). It:
- **Builds on Credential Management Level 1**, extending the `navigator.credentials` surface with a `digital` member.
- Is **protocol-agnostic**, but defines a **registry of supported protocols** (e.g., OpenID4VP over Digital Credentials API).
- Treats the browser as a **trusted mediator**: it can inspect requests, enforce permissions, and show UX that keeps users in control.
Key idea: The digital credentials API is not itself a “credential protocol”. It’s a browser API that carries credential protocols between the website and the wallet.
## Key Actors: Website, User, User Agent (Browser) … and Wallet
A typical Digital Credentials API interaction involves:
### Website (Verifier or Issuer)
The web app calling navigator.credentials.get() (for presentation) or navigator.credentials.create() (for issuance). It knows what claims it needs (e.g. age_over_21, full_name).
### User (Holder)
The person who owns the credentials. They see the browser’s consent UI and decide whether to share or accept a credential.
### User Agent (Browser)
The gatekeeper. It:
- Validates the origin.
- Mediates access to the wallet(s) and identity APIs on the platform.
- Presents a credential chooser / consent UI.
### Wallet / Credential Store
A platform wallet (e.g. Apple Wallet, Google Wallet) or another app registered as a wallet. It holds the actual credentials and builds the cryptographic presentation or issuance response.
This is a shift from “mobile-app + QR codes” flows to browser-native credential flows, but the underlying issuer-holder-verifier model stays the same.
## How Digital Credentials API (DC-API) Works (A Step-by-Step Flow)
The **DC-API** protocol defines a clear sequence of interactions between the website, the browser, and the user's credential store. While the underlying implementation may vary depending on the browser and wallet integration, the developer-facing API remains consistent and straightforward.
### Step 1: The Website Requests a Credential
The process begins when a website needs to request a credential from the user. The website's JavaScript code calls the **DC-API**, typically using a method like `navigator.credentials.get()` with a credential request object. This request specifies:
- The type of credential needed (e.g., a government-issued ID, a university diploma)
- The specific claims required (e.g., `age_over_21`, `full_name`)
- The expected origin(s) for security validation
- Any protocol-specific parameters (if using **OpenID4VCI** or **OpenID4VP** under the hood)
The browser receives this request and validates that it comes from an authorized origin, preventing malicious websites from requesting credentials without user consent.
### Step 2: The Browser Presents the Request to the User
The browser acts as the user's agent, presenting the credential request in a secure, user-friendly interface. This interface shows:
- Which website is requesting the credential
- What specific information is being requested
- What credential will be used to satisfy the request
- Clear options to approve or deny the request
This step is crucial for user privacy and security. The browser ensures that users have full visibility into what is being requested and who is requesting it, preventing phishing attacks and ensuring informed consent.
### Step 3: The User Approves and the Browser Retrieves the Credential
If the user approves the request, the browser securely communicates with the user's credential store (which may be built into the browser, a browser extension, or a connected mobile wallet). The credential store:
1. Locates the appropriate credential(s) that satisfy the request
2. Constructs the necessary presentation or issuance response (depending on whether this is a presentation or issuance flow)
3. Applies [selective disclosure](https://docs.walt.id/concepts/selective-disclosure.md) if only specific claims are needed
4. Signs the response with the user's private key (for holder binding)
5. Returns the credential data to the browser
### Step 4: The Browser Returns the Credential to the Website
The browser receives the credential from the credential store and returns it to the website's JavaScript code via the **DC-API** response. The website can then:
- Validate the cryptographic signatures
- Extract the claims it needs
- Proceed with the business logic (e.g., granting access, completing a purchase)
The entire flow happens within the browser, creating a seamless user experience while maintaining the security and privacy guarantees of verifiable credentials.
## Digital Credentials API (DC-API) vs. OpenID4VCI/VP: Understanding the Relationship
A common point of confusion for developers is understanding how **DC-API** relates to **OpenID4VCI** and **OpenID4VP**. The relationship is complementary rather than competitive:
### OpenID4VCI/VP: The Protocol Layer
**OpenID4VCI** and **OpenID4VP** are protocol specifications that define the exact messages, endpoints, and cryptographic mechanisms for credential issuance and presentation. They specify:
- How authorization requests are structured
- What endpoints wallets and issuers/verifiers must implement
- How cryptographic proofs are created and validated
- The exact format of credential offers, presentation requests, and responses
These protocols are transport-agnostic—they can work over HTTP, mobile deep links, QR codes, or any other communication channel.
### Digital Credentials API (DC-API): The Browser API Layer
**DC-API** is a browser API that provides a JavaScript interface for web developers. It abstracts away the underlying protocol details, allowing developers to request credentials without needing to understand the intricacies of **OpenID4VCI** or **OpenID4VP**.
**DC-API** can use **OpenID4VCI** or **OpenID4VP** as its underlying protocol, but it could also work with other protocols. The browser implementation handles the protocol-specific details, translating between the simple JavaScript API calls and the complex protocol messages.
### How They Work Together
Consider a website that needs to verify a user's age:
1. **Website constructs an authorization request**: The website constructs an authorization request (using a Verifier Server which supports OpenID4VP), including a [DCQL](https://docs.walt.id/concepts/data-exchange-protocols/dcql.md) query specifying the required credential and claims needed for the age verification.
2. **Browser uses DC-API**: The website's JavaScript calls `navigator.credentials.get()` with the mapped object from the authorization request.
3. **Wallet processes the request**: The user's wallet receives the **OpenID4VP** request, matches it against stored credentials, and creates a verifiable presentation.
4. **Browser returns via DC-API**: The browser receives the **OpenID4VP** response and translates it back into a simple JavaScript object that the website then sends back to the Verifier Server.
5. **Verifier Server validates the response**: The Verifier Server validates the response and returns the result to the website.
6. **Website displays the result**: The website displays the result to the user.
This layered architecture provides the best of both worlds: the standardization and interoperability of **OpenID4VCI/VP** at the protocol level, combined with the developer-friendly simplicity of **DC-API** at the application level.
| Feature | OpenID4VCI/VP | DC-API |
|---------|---------------|--------|
| **Layer** | Protocol specification | Browser API |
| **Transport** | Protocol-agnostic (HTTP, QR codes, deep links) | Browser-based (JavaScript) |
| **Developer Experience** | Requires understanding of protocol details | Simple JavaScript API calls |
| **Use Case** | Mobile wallets, backend services, cross-device flows | Web applications, same-device browser flows |
| **Protocol Support** | Defines specific protocol | Can use OpenID4VCI/VP or other protocols |
## Real-World Digital Credentials API (DC-API) Use Cases
The true value of **DC-API** lies in its ability to solve real-world problems with a seamless user experience. Here are some practical examples:
### Use Case 1: Age Verification for E-Commerce
**The Problem**: Online retailers selling age-restricted products (alcohol, tobacco, adult content) need to verify customer age, but traditional methods are either insecure (self-reported age) or intrusive (uploading full ID documents).
**The DC-API Solution**: An e-commerce website can use **DC-API** to request a single claim: `age_over_21: true`. The user's browser presents a clear consent prompt, and if approved, the website receives cryptographically verified proof of age without learning the user's name, address, or exact date of birth. This enables compliance with age restrictions while maximizing user privacy.
### Use Case 2: Digital Credential Issuance for Online Learning
**The Problem**: Educational platforms want to issue digital certificates and diplomas, but the current process often requires manual verification, email delivery, and separate credential management systems.
**The DC-API Solution**: After a student completes a course, the learning platform can use **DC-API** to issue a verifiable credential directly to the student's browser-based wallet. The student receives their credential instantly, and it's cryptographically signed and tamper-evident. The student can then use **DC-API** to present this credential to employers or other educational institutions, all within their browser.
### Use Case 3: Streamlined KYC for Financial Services
**The Problem**: Financial institutions must perform Know Your Customer (KYC) checks, but traditional processes require customers to upload sensitive documents, wait for manual verification, and often share more information than necessary.
**The DC-API Solution**: A bank's website can use **DC-API** to request specific KYC claims from a government-issued digital ID. The user sees exactly what information is being requested, approves the request, and the bank receives cryptographically verified identity data in seconds. This dramatically reduces onboarding time, improves security, and enhances user privacy through selective disclosure.
## Current Status and Browser Support (2026)
**Standardization Status**
• The W3C Digital Credentials API is currently a Working Draft on the W3C Recommendation track.
• The **Digital Credentials API** is maintained by the Federated Identity Working Group and was first published as a Public Working Draft in July 2025; the latest Working Draft (December 2025) continues to refine the model, security, privacy, and registry of supported protocols.
**Browser & Platform Implementations**
Support is still emerging, but already quite real:
- Chrome (Android & Desktop)
- DC-API is available as an origin trial starting in Chrome 128 on Android for same-device presentation flows.
- From Chrome 136, cross-device support lets desktop Chrome display a QR code that the user scans with an Android phone; credentials are then presented from the phone’s wallet back to the desktop site.
- Access currently requires enabling a flag and/or participation in the origin trial, but the pattern is clear: DC-API is the primary web surface for Android wallets.
- Safari & WebKit (Apple Platforms)
- Safari 26 on macOS, iOS, and iPadOS introduces web support for the W3C Digital Credentials API.
- Websites can request mobile IDs (e.g., a state ID in Apple Wallet) and get a signed, encrypted response.
- Safari also supports cross-device flows where a Mac or iPad uses an iPhone as the wallet device via a QR-based handoff.
- Developer Sandbox & Test Verifier
- The digitalcredentials.dev site and tools provide cross-browser demos, test verifiers, and documentation for experimenting with DC-API and OpenID4VP.
In practice, DC-API support is good enough for pilots and developer experimentation. For production systems, you should:
- Implement feature detection & graceful fallback.
- Avoid assuming a specific browser or OS.
- Keep up with spec changes (the API is still a Working Draft).
## Security and Privacy Considerations
**DC-API** is designed with security and privacy as fundamental principles. Key security features include:
### Origin Validation
The browser validates that credential requests come from authorized origins, preventing malicious websites from requesting credentials without user consent. The `expectedOrigins` parameter allows websites to specify which origins are allowed to receive credential responses, providing an additional layer of protection against cross-origin attacks.
### User Consent and Transparency
Every credential request must be explicitly approved by the user through a browser-mediated consent interface. Users see exactly what information is being requested and which website is making the request, ensuring informed consent and preventing phishing attacks.
### Cryptographic Security
**DC-API** leverages the same cryptographic security mechanisms as **OpenID4VCI** and **OpenID4VP**, including:
- **Holder Binding**: Credentials are cryptographically bound to keys controlled by the user, preventing credential theft and replay attacks.
- **Selective Disclosure**: Users can share only the specific claims needed, minimizing data exposure.
- **Tamper Evidence**: Credentials are cryptographically signed, making any tampering immediately detectable.
### Protocol-Level Security
It's important to note that **DC-API** does not provide data integrity on credential requests or responses itself—this responsibility lies with the underlying protocol (e.g., **OpenID4VCI** or **OpenID4VP**). The API provides a secure transport mechanism, but the cryptographic guarantees come from the credential format and exchange protocol being used.
## Practical Implementation: A Hands-On Walkthrough
For developers building web applications that need to request credentials, implementing **DC-API** involves calling the browser API and handling the response. For a complete implementation example, check out the [walt.id Digital Credential Button project](https://github.com/walt-id/waltid-identity/tree/main/waltid-applications/waltid-digital-credentials), which provides a reference implementation for invoking the Browser-based DC-API.
## Digital Credentials API (DC-API) FAQ
### Is DC-API a W3C standard yet?
Not yet. As of late 2025, the Digital Credentials API is a W3C Working Draft on the Recommendation track. It is stable enough for experimentation, but you should expect some changes in the IDL, registry details, and protocol integration over time.
### What is the relationship between DC-API and Credential Management Level 1?
**DC-API** builds upon the **Credential Management Level 1** specification, which defines a general framework for credential storage and retrieval in browsers. **DC-API** extends this framework specifically for verifiable credentials, adding support for cryptographic proofs, selective disclosure, and protocol-agnostic credential exchange.
### Can DC-API work with mobile wallets?
Yes, **DC-API** can work with mobile wallets through browser extensions or connected services. The browser acts as the intermediary, communicating with the mobile wallet to retrieve credentials. This allows web applications to leverage the security and convenience of mobile wallet implementations while providing a seamless browser-based user experience.
### Is DC-API only for credential presentation, or can it also be used for issuance?
**DC-API** supports both credential issuance and presentation. The same API can be used to request credentials from users (presentation) or to issue new credentials to users (issuance), depending on the underlying protocol being used (**OpenID4VP** for presentation, **OpenID4VCI** for issuance).
### How does DC-API compare to WebAuthn?
**WebAuthn** is a browser API for passwordless authentication using public key cryptography. **DC-API** is a browser API for exchanging verifiable credentials. While both use similar browser infrastructure (Credential Management), they serve different purposes:
- **WebAuthn**: Authenticates users ("prove you are who you claim to be")
- **DC-API**: Exchanges credentials ("share verified attributes about yourself")
They can be used together—for example, a website might use **WebAuthn** for authentication and **DC-API** for requesting additional verified attributes.
### What happens if a user doesn't have the required credential?
If the user doesn't have a credential that satisfies the request, the browser will return a `NotFoundError`. Your application should handle this gracefully, potentially:
- Falling back to an alternative verification method
- Redirecting the user to an issuer where they can obtain the required credential
- Providing clear instructions on how to obtain the credential
## Next Steps
- [OpenID4VP](https://docs.walt.id/concepts/data-exchange-protocols/openid4vp.md) - Explore the presentation protocol that DC-API can use under the hood.
- [OpenID4VCI](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md) - Learn about the issuance protocol for credentials.
- [How to verify credentials via Digital Credentials API (Community Stack)](https://docs.walt.id/community-stack/verifier2/credential-verification/dc-api.md) - Check out the walt.id Verifier API docs for DC-API verification flows.
- [How to verify credentials via Digital Credentials API (Enterprise Stack)](https://docs.walt.id/enterprise-stack/services/verifier2-service/credential-verification/dc-api.md) - Check out the walt.id Verifier API docs for DC-API verification flows, trusted by :traction-devs{} developers and organizations worldwide.
### Additional Resources
- [W3C Digital Credentials API Working Draft and its integration with Credential Management and the protocol registry](https://www.w3.org/TR/digital-credentials/)
- [W3C blog announcing the Digital Credentials API and noting early implementations from Google and Apple](https://www.w3.org/blog/2025/w3c-digital-credentials-api-publication-the-next-step-to-privacy-preserving-identities-on-the-web/)
- [Chrome origin trial announcements for the Digital Credentials API on Android and cross-device from desktop Chrome, including usage details and flags](https://developer.chrome.com/blog/digital-credentials-api-origin-trial)
- [WebKit blog introducing Digital Credentials API support in Safari 26 across Apple platforms and describing same-device and cross-device flows](https://webkit.org/blog/17431/online-identity-verification-with-the-digital-credentials-api/)
--------------------------------------------------------------------------------
PAGE: What is the HAIP Profile? The Developer's Guide (2026)
URL: https://docs.walt.id/concepts/data-exchange-protocols/haip
RAW: https://docs.walt.id/concepts/data-exchange-protocols/haip.md
STACK: Concepts (stack-agnostic, applies to both Community and Enterprise Stack)
--------------------------------------------------------------------------------
# OpenID4VC HAIP Profile: A Developer's Guide to High-Assurance Digital Credentials
In the rapidly evolving world of digital identity, interoperability and security are often at odds. Different wallets, issuers, and verifiers implement the same specifications in subtly different ways, leading to fragmentation and security gaps. The **OpenID4VC High Assurance Interoperability Profile (HAIP) v1.0** solves this problem by defining a strict, security-focused profile that ensures all parties in a credential ecosystem speak the same language—and speak it securely.
This guide provides a comprehensive walkthrough of HAIP v1.0: what it is, what it enforces, and why it's becoming the foundation for high-assurance digital identity ecosystems worldwide.
## What is the HAIP Profile? (The Simple Explanation)
**HAIP (High Assurance Interoperability Profile)** is a technical specification published by the OpenID Foundation that defines a strict subset of the OpenID for Verifiable Credentials (OpenID4VC) specifications. Think of it as a "security-hardened configuration" for digital credential systems.
While [OpenID4VCI](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md) and [OpenID4VP](https://docs.walt.id/concepts/data-exchange-protocols/openid4vp.md) define flexible protocols with many optional features, HAIP takes these specifications and says: "If you want high security and guaranteed interoperability, here's exactly what you MUST implement."
The profile was approved as a Final Specification by the OpenID Foundation membership on December 24, 2025, with 87 approval votes. It successfully passed interoperability testing in November 2025, achieving **98% passing rates** on credential presentation workflows—demonstrating that the specification delivers on its promise of interoperability.
## TL;DR:
- **HAIP** is a security-focused profile of OpenID4VCI and OpenID4VP
- **HAIP** mandates specific credential formats: **[SD-JWT VC](https://docs.walt.id/concepts/digital-credentials/sd-jwt-vc.md)** and **[ISO mdoc](https://docs.walt.id/concepts/digital-credentials/mdoc-mdl-iso.md)**
- **HAIP** requires **FAPI 2.0 security controls**, including DPoP and PKCE
- **HAIP** enforces **X.509 certificate-based trust** for issuer and verifier authentication
- **HAIP** is designed for ecosystems requiring **high security and privacy**, including [eIDAS 2.0](https://walt.id/eidas2) implementations
## Why Does HAIP Exist? The Problem It Solves
### The Interoperability Challenge
Digital identity specifications like OpenID4VCI and OpenID4VP are intentionally flexible. They support multiple credential formats, various authentication methods, and numerous optional features. This flexibility is valuable for adoption, but it creates a problem: two implementations can both be "compliant" with the specification yet completely unable to communicate with each other.
Consider a scenario where:
- **Wallet A** supports only SD-JWT VC credentials with `jwk` binding
- **Wallet B** supports only ISO mdoc credentials
- **Issuer X** uses unsigned metadata and pre-authorized code flow
- **Issuer Y** requires signed metadata and authorization code flow with PAR
All four implementations might be valid according to the base specifications, but they can't interoperate. This fragmentation undermines the entire value proposition of standardized digital credentials.
### The Security Gap
Beyond interoperability, the base specifications allow configurations that may not meet the security requirements of high-assurance use cases. For example:
- Optional encryption of responses could be skipped
- Weak cryptographic algorithms might be used
- Trust establishment mechanisms could be inconsistent
HAIP addresses both challenges by defining a **mandatory baseline** that ensures both interoperability and security.
## Core Security Goals of HAIP
HAIP v1.0 targets two primary security properties that are essential for high-assurance digital identity:
### 1. Authenticity of Claims
There must be **strong assurance that the claims within a Credential or Presentation are valid and bound to the correct Holder**. This encompasses:
- **Issuance Security**: The process by which credentials are created and delivered must be secure
- **Credential Protection**: Credentials must be protected both at rest (in the wallet) and during presentation
- **Issuer Trust**: Verifiers must have access to trustworthy information about the Issuer to validate credentials
### 2. Holder Authentication
There must be **strong assurance that the Credential is presented by its legitimate Holder** in a given transaction. This involves:
- **Proof of Holder Binding**: Cryptographic proof that the presenter controls the key bound to the credential
- **Claim-based Binding**: Support for additional binding mechanisms built on top of cryptographic binding
These security goals directly address the most critical threats in digital identity systems: credential theft, impersonation, and unauthorized disclosure.
## What HAIP Enforces: The Technical Requirements
HAIP v1.0 profiles four core specifications and defines mandatory requirements for each. Let's examine what the profile enforces across the credential lifecycle.
### Credential Formats: SD-JWT VC and ISO mdoc
HAIP mandates support for at least one of two credential formats:
**IETF SD-JWT VC** (Selective Disclosure JWT Verifiable Credentials):
- Compact serialization MUST be supported
- The `cnf` claim MUST include a JWK for cryptographic holder binding
- Status management via Token Status List is required when using the `status` claim
- X.509 certificate chain MUST be included in the `x5c` JOSE header for issuer key resolution
**ISO mdoc** (Mobile Document as defined in ISO/IEC 18013-5):
- Used for mobile Driver's Licenses (mDL) and similar documents
- DeviceResponse structure for presentations
- MSO revocation mechanisms as defined in ISO/IEC 18013-5
Both formats support [selective disclosure](https://docs.walt.id/concepts/selective-disclosure.md), allowing holders to share only the specific claims required for a transaction—a fundamental privacy feature.
### OpenID4VCI Requirements (Credential Issuance)
For credential issuance, HAIP enforces:
**Authorization Flow**:
- Authorization code flow MUST be supported
- PKCE with `S256` challenge method is REQUIRED
- Pushed Authorization Requests (PAR) MUST be used when using the Authorization Endpoint
- DPoP (Demonstrating Proof of Possession) MUST be supported for sender-constrained access tokens
**Wallet Attestation**:
- Wallets MUST authenticate at OAuth2 endpoints using Wallet Attestation
- Attestations MUST NOT be reused across different Issuers (preventing tracking)
- X.509 certificates MUST be used for attestation validation
**Key Attestation**:
- Wallets MUST support key attestations to prove key properties
- The `jwt` proof type with `key_attestation` and `attestation` proof type are supported
- X.509 certificates validate key attestation signatures
**Issuer Metadata**:
- Credential Issuer metadata MUST include a scope for every Credential Configuration
- When higher assurance is required, signed Credential Issuer Metadata MUST be supported
- X.509 certificate-based key resolution for metadata validation
### OpenID4VP Requirements (Credential Presentation)
For credential presentation, HAIP enforces:
**Request Authentication**:
- Verifiers MUST use the `x509_hash` Client Identifier Prefix for signed requests
- X.509 certificates authenticate the Verifier to the Wallet
- Self-signed certificates are NOT allowed
**Query Language**:
- [DCQL](https://docs.walt.id/concepts/data-exchange-protocols/dcql.md) (Digital Credentials Query Language) MUST be used for credential requests
- Authority Key Identifier (AKI)-based Trusted Authority Query MUST be supported
**Response Encryption**:
- Response encryption is MANDATORY
- ECDH-ES with P-256 curve for key agreement
- A128GCM and A256GCM encryption algorithms
- Ephemeral encryption keys specific to each request
**Same-Device Flow**:
- Verifiers and Wallets MUST support same-device flow
- Redirect back to Verifier is REQUIRED for session binding
- Verifiers MUST reject presentations if redirect is not followed
**W3C Digital Credentials API**:
- Support for browser-based credential presentation via [DC-API](https://docs.walt.id/concepts/data-exchange-protocols/dc-api.md)
- Response mode `dc_api.jwt` MUST be supported
- Unsigned, signed, and multi-signed requests are supported
### Cryptographic Requirements
HAIP mandates specific cryptographic algorithms to ensure consistent security:
**Digital Signatures**:
- ECDSA with P-256 and SHA-256 (`ES256`) MUST be supported at minimum
- COSE algorithm identifiers `-7` or `-9` for mdoc contexts
**Hash Algorithms**:
- SHA-256 MUST be supported for all digest operations
**Key Sizes**:
- Implementations must follow guidance from NIST SP 800-131A, NIST SP 800-57, or BSI TR-02102-1
## HAIP and eIDAS 2.0: The Regulatory Connection
One of the most significant drivers for HAIP adoption is the European Union's eIDAS 2.0 regulation, which mandates that by 2026, every EU member state must offer a European Digital Identity Wallet (EUDI Wallet).
HAIP is explicitly designed to support eIDAS 2.0 implementations. The specification notes:
> "This specification fulfils some, but not all, of the requirements to meet the 'High' Level of Assurance (LoA) as defined in the eIDAS Regulation. While this specification defines features intended for scenarios targeting a high level of security, these features must be combined with additional measures outside of the scope of HAIP to achieve LoA High compliance."
This means HAIP provides the **technical foundation** for eIDAS 2.0 compliance, but ecosystems must layer additional policy and procedural requirements on top.
### Trust Establishment
At the foundation, HAIP uses **X.509 PKI** (Public Key Infrastructure) for trust establishment across all parties:
1. **Issuer Trust**: Credentials include the issuer's X.509 certificate chain in the `x5c` header, allowing verifiers to validate the issuer's identity against trusted root certificates
2. **Verifier Trust**: Presentation requests are signed with X.509 certificates, allowing wallets to verify the verifier's identity before sharing credentials
3. **Wallet Trust**: Wallet Attestations prove the wallet's authenticity to issuers, preventing rogue applications from obtaining credentials
4. **Key Trust**: Key Attestations prove that cryptographic keys are properly protected (e.g., in secure hardware)
### Credential Protection
HAIP ensures credentials are protected throughout their lifecycle:
- **At Issuance**: DPoP tokens prevent token theft; Wallet Attestation ensures only legitimate wallets receive credentials
- **At Rest**: Holder binding ensures credentials are cryptographically bound to keys the holder controls
- **At Presentation**: Response encryption prevents eavesdropping; session binding prevents phishing
## Ecosystem Extension Points
HAIP intentionally leaves certain decisions to ecosystems, recognizing that different contexts have different requirements:
| Extension Point | Options |
|----------------|---------|
| **Flows** | Presentation only, issuance only, or both |
| **Presentation Method** | DC-API, redirects with custom URL schemes, or claimed HTTPS URIs |
| **Credential Format** | SD-JWT VC, ISO mdoc, or both |
| **Issuer Metadata** | Signed or unsigned |
| **Credential Offer Delivery** | QR code, deep link, or other mechanisms |
| **Attestation Formats** | HAIP-defined or ecosystem-specific |
| **X.509 Profiles** | Ecosystem-defined certificate policies |
| **Additional Crypto** | Ecosystem may mandate additional algorithms |
This flexibility allows HAIP to serve as a foundation that ecosystems can build upon while maintaining baseline interoperability.
## Practical Implementation Considerations
### Custom URL Schemes
HAIP defines two custom URL schemes for wallet invocation:
- `haip-vci://` - For credential issuance flows
- `haip-vp://` - For credential presentation flows
These schemes allow verifiers and issuers to invoke HAIP-compliant wallets directly.
## Real-World HAIP Use Cases
### Government Digital Identity
A national government implementing digital identity cards can use HAIP to ensure:
- All approved wallets meet security requirements via Wallet Attestation
- Citizens can present credentials to any compliant verifier
- Credentials cannot be stolen or impersonated due to holder binding
- Privacy is protected through selective disclosure
### Financial Services KYC
Banks implementing digital KYC can use HAIP to:
- Receive cryptographically verified identity attributes
- Trust that credentials come from authorized issuers via X.509 trust chains
- Ensure presentations are encrypted and session-bound
- Meet regulatory requirements for identity verification
### Cross-Border Credential Recognition
International credential recognition (e.g., EU-US driver's license verification) can use HAIP to:
- Establish mutual trust through X.509 certificate hierarchies
- Ensure consistent security regardless of issuing country
- Support both SD-JWT VC and ISO mdoc formats for flexibility
## HAIP FAQ
### Does HAIP replace OpenID4VCI and OpenID4VP?
No. HAIP is a **profile** of these specifications, not a replacement. It defines which features of OpenID4VCI and OpenID4VP must be implemented and how they must be configured for high-assurance use cases.
### Is HAIP required for eIDAS 2.0 compliance?
HAIP provides a strong technical foundation for eIDAS 2.0, but it alone is not sufficient for full compliance. Additional policy, procedural, and potentially technical measures are required to achieve "High" Level of Assurance under eIDAS.
### Can I use HAIP for lower-assurance use cases?
Yes. The specification notes that while it's aimed at high-assurance use cases, it can also be used for lower-assurance scenarios. The security features don't hurt lower-assurance use cases—they just provide more protection than strictly necessary.
### What if my ecosystem needs features not in HAIP?
HAIP is designed to be extended. Ecosystems can add requirements on top of HAIP while maintaining baseline interoperability with other HAIP-compliant implementations.
### How does HAIP handle offline scenarios?
HAIP v1.0 focuses on online scenarios. Offline presentation (e.g., over BLE) is explicitly listed as out of scope for this version but may be addressed in future versions.
## Next Steps
- [OpenID4VCI](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md) - Understand the credential issuance protocol that HAIP profiles
- [OpenID4VP](https://docs.walt.id/concepts/data-exchange-protocols/openid4vp.md) - Learn about the presentation protocol and DCQL queries
- [DC-API](https://docs.walt.id/concepts/data-exchange-protocols/dc-api.md) - Explore browser-based credential presentation supported by HAIP
- [SD-JWT VC](https://docs.walt.id/concepts/digital-credentials/sd-jwt-vc.md) - Deep dive into the SD-JWT VC credential format
- [ISO mdoc](https://docs.walt.id/concepts/digital-credentials/mdoc-mdl-iso.md) - Learn about the ISO mobile document format
- [walt.id Issuer & Verifier APIs](https://docs.walt.id/community-stack/home.md) - Build HAIP-compliant issuance and verification flows, trusted by :traction-devs{} developers and organizations worldwide.
### Additional Resources
- [HAIP v1.0 Final Specification](https://openid.net/specs/openid4vc-high-assurance-interoperability-profile-1_0.html) - The official specification document
- [OpenID Foundation HAIP Announcement](https://openid.net/openid4vc-high-assurance-interoperability-profile-haip-1-0-final-specification-approved/) - Official announcement of HAIP 1.0 approval
- [HAIP GitHub Repository](https://github.com/openid/oid4vc-haip) - Source repository for the specification
- [OIDF Interoperability Testing Results](https://openid.net/haip-1-0-openid4vp-1-0-achieve-98-in-oidf-interop-testing/) - Results from the November 2025 interop event
--------------------------------------------------------------------------------
PAGE: DCQL: The Developer's Guide to the Digital Credentials Query Language
URL: https://docs.walt.id/concepts/data-exchange-protocols/dcql
RAW: https://docs.walt.id/concepts/data-exchange-protocols/dcql.md
STACK: Concepts (stack-agnostic, applies to both Community and Enterprise Stack)
--------------------------------------------------------------------------------
# DCQL: The Digital Credentials Query Language
**DCQL (Digital Credentials Query Language)** is the mechanism verifiers use inside an [OpenID4VP](https://docs.walt.id/concepts/data-exchange-protocols/openid4vp.md) request to express exactly which credentials a wallet should present, and in what combinations. It was introduced in OpenID4VP Draft 22 (October 2024) and is part of the final `v1.0` specification, replacing the older Presentation Exchange / Presentation Definition approach.
This guide explains how DCQL is structured, what each part does, and where the common implementation mistakes occur.
## The Mental Model
DCQL is the verifier's answer to one specific question: "out of the credentials the wallet holds, which ones — and in what combinations — can satisfy what I need from this transaction?"
There are only ever two layers of decision:
1. **A list of credential queries** — "I want a credential that looks like X."
2. **(Optional) logic over those queries** — "I need (A and B), or (A and C), but never just A on its own."
That is the entire shape of the language. Every detail in the specification hangs off those two ideas.
**Compared to Presentation Exchange** (which DCQL replaces), this is dramatically simpler. Presentation Exchange required JSON Schema-like constraint descriptors with JSONPath fragments and nested filter blocks. DCQL replaces that constraint-language sprawl with named fields, a single claim path syntax, and explicit AND/OR operators.
## The Top-Level Shape
Every DCQL query is an object with two possible keys:
```json
{
"credentials": [ ... ],
"credential_sets": [ ... ]
}
```
- `credentials` is **required** — your list of "I want a credential like X" entries.
- `credential_sets` is **optional** — AND/OR logic across those entries.
If you only provide `credentials` and no `credential_sets`, the wallet must satisfy every entry — an implicit AND. You add `credential_sets` only when you need OR, optionality, or alternative combinations.
**Note:**
Reaching for `credential_sets` too early is where most implementations over-engineer. If the use case is "give me a PID and an employment credential," do not reach for `credential_sets`. Two entries in `credentials` already expresses that.
## Anatomy of a Credential Query
Each entry in `credentials` follows this structure:
```json
{
"id": "digitalID",
"format": "dc+sd-jwt",
"meta": { "vct_values": ["https://issuer.demo.walt.id/draft13/digitalID"] },
"claims": [ ... ],
"claim_sets": [ ... ],
"trusted_authorities": [ ... ],
"require_cryptographic_holder_binding": true,
"multiple": false
}
```
The required fields are `id`, `format`, and `meta`. Everything else is optional and each unlocks one specific capability.
| Field | Required | Description |
|---|---|---|
| `id` | Yes | Your handle for this query. The wallet keys its `vp_token` response by these ids. Must be unique within the request. |
| `format` | Yes | Credential format identifier: `dc+sd-jwt`, `mso_mdoc`, `jwt_vc_json`. |
| `meta` | Yes | Format-specific filter. The structure differs per format — see the [Format-Specific Gotchas](#format-specific-gotchas) section below. |
| `claims` | No | Limits which claims the wallet should disclose. Omitting this is not the same as requesting everything — see below. |
| `claim_sets` | No | Alternative combinations of claims within this single credential. |
| `trusted_authorities` | No | Restricts matches to credentials from specific trust anchors. |
| `require_cryptographic_holder_binding` | No | Defaults to `true`. Set `false` only when a key binding proof is genuinely not needed. |
| `multiple` | No | Defaults to `false`. Set `true` to allow the wallet to return more than one matching credential of the same type. |
The minimum viable query is exactly the three required fields and nothing else. The wallet returns the entire credential.
## Selective Disclosure: Claims and the Claims Path Pointer
Add a `claims` array to tell the wallet which fields you actually need:
```json
"claims": [
{ "path": ["given_name"] },
{ "path": ["family_name"] },
{ "path": ["age_over_18"] }
]
```
### The `path` syntax
`path` is an array, not a string. Each segment is one of:
- A **string** — an object key (e.g. `"address"`)
- An **integer** — an array index
- **`null`** — matches all elements at that array level
So `["address", "locality"]` means "the `locality` field inside the `address` object." This is the Claims Path Pointer defined in §7 of the spec. It is not JSONPath, it is not JSON Pointer — it is its own syntax and you will trip on it if you assume otherwise.
### Format affects path semantics
For JSON-based credentials (`dc+sd-jwt`, `jwt_vc_json`), paths walk the credential's claim structure as you'd expect.
For [**mdoc**](https://docs.walt.id/concepts/digital-credentials/mdoc-mdl-iso.md), the rule is explicit: the first segment is the **namespace**, the second is the **element identifier**, and that is the entire path — mdoc paths are always exactly two segments long. So for an mDL `given_name`, you write:
```json
{ "path": ["org.iso.18013.5.1", "given_name"] }
```
### What "no claims" means
If `claims` is absent, you are requesting no selectively-disclosable claims. For an SD-JWT VC, the wallet returns only the always-disclosed envelope and the key binding JWT. Omitting `claims` does **not** mean "give me everything."
## Value Matching with `values`
You can constrain a claim to a specific value:
```json
{ "path": ["nationality"], "values": ["AT", "DE"] }
```
The wallet should return the claim only if its type and value match at least one of the listed values. Strings, integers, and booleans are accepted.
**Info:**
Value matching leaks information about claim non-possession. If the wallet refuses because the nationality is not in the list, the verifier learns the holder's nationality is something else.
For mdoc credentials, CBOR values must be JSON-converted (per RFC 8949 §6.1) before matching. That conversion is not always lossless — date types, byte strings, and similar types can behave unexpectedly. Do not rely on value matching for mdoc data types beyond the simple primitives.
## `claim_sets` — Alternative Claim Combinations Within One Credential
`claim_sets` answers the question: "which combinations of claims would satisfy me from this single credential?"
```json
"claims": [
{ "id": "gn", "path": ["given_name"] },
{ "id": "fn", "path": ["family_name"] },
{ "id": "dob", "path": ["birth_date"] },
{ "id": "over18", "path": ["age_over_18"] }
],
"claim_sets": [
["gn", "fn", "over18"],
["gn", "fn", "dob"]
]
```
Read this as: "I would accept either {given_name, family_name, age_over_18} or {given_name, family_name, birth_date}." The order of the sets expresses the verifier's **preference** — the wallet should return the first option it can satisfy, but is not strictly required to. Put the set you most prefer first: if you want to signal a preference for minimum disclosure, list the smaller or more privacy-preserving set earlier.
Once `claim_sets` is used, the `id` field on each entry in `claims` becomes mandatory — the sets reference claims by those ids.
`claim_sets` is for **alternative claim combinations within one credential**. It is not a mechanism for making individual claims optional. DCQL has no concept of an optional claim by design.
If you need to express an optional claim, there are two patterns:
1. **Within a single credential query** — use `claim_sets` with the "richer" set first and the minimal set as a fallback. The wallet will return the richer set if it can satisfy it, and fall back to the minimal one if not. This is not true optionality (the user has no choice), but it gracefully handles credentials that may or may not contain a given claim.
2. **Across two credential queries** — model the optional claim as a separate credential query inside an optional `credential_set` (see the next section). This is the more explicit pattern and ties the optionality to a declared purpose.
## Multiple Credentials — Implicit AND
Two entries in `credentials` with no `credential_sets`:
```json
{
"credentials": [
{ "id": "pid", "format": "dc+sd-jwt", "meta": { ... }, "claims": [ ... ] },
{ "id": "employment", "format": "dc+sd-jwt", "meta": { ... }, "claims": [ ... ] }
]
}
```
Both must be satisfied. The `vp_token` in the response will be an object with two keys — `pid` and `employment` — each holding a presentation. The keys are exactly the `id` values you chose in the query, so pick readable names.
By default, each key maps to a single presentation. If you set `multiple: true` on a credential query, the wallet may return an array of presentations for that key instead.
## `credential_sets` — Explicit Logic Across Credentials
`credential_sets` is where you express OR, optionality, and alternative credential combinations. Each set has two properties:
```json
"credential_sets": [
{
"options": [ ["pid"], ["mdl"] ],
"required": true
}
]
```
- `options` is an array of arrays. Each inner array is one acceptable combination of credential ids. The wallet must satisfy at least one inner array in full.
- `[["pid"], ["mdl"]]` reads as "(pid) OR (mdl)."
- To express "(pid AND id_card) OR (mdl)" you write `[["pid", "id_card"], ["mdl"]]`.
- `required` defaults to `true`. Setting it to `false` makes the entire set optional.
**Modelling optional credentials** — "give me a PID, and if you happen to have a diploma, include it":
```json
"credential_sets": [
{ "options": [["pid"]] },
{ "required": false, "options": [["diploma"]] }
]
```
**Note:**
When you use `credential_sets`, the sets become the sole source of truth for what is required. The `credentials` array defines the universe of possible credential shapes — their format, claims, and constraints — but it is the sets that determine what actually gets requested. Entries in `credentials` that are not referenced by any set are inert: the wallet has no instruction to return them.
## Trust Framework Filtering with `trusted_authorities`
For EUDI and other regulated ecosystems, `trusted_authorities` is where policy constraints end up:
```json
"trusted_authorities": [
{ "type": "etsi_tl", "values": ["https://eu-lotl.example/lotl.xml"] }
]
```
Three authority types are defined:
| Type | Description |
|---|---|
| `aki` | Authority Key Identifier, base64url-encoded. The values are the key identifiers of the trusted issuer CA certificates (their Subject Key Identifier). A credential matches if its certificate chain contains a certificate whose AKI extension's keyIdentifier component references one of those trusted CAs. Use when trusting a specific issuer CA key directly. |
| `etsi_tl` | An ETSI Trusted List URL (TS 119 612). The wallet walks the LOTL/TL cascade and matches if any cert in the credential's chain appears on the referenced trust list. The natural fit for QTSP-issued credentials under eIDAS. |
| `openid_federation` | An Entity Identifier. The wallet must be able to construct a valid trust chain from the credential's issuer up to that entity. |
**Note:**
The spec is explicit: `trusted_authorities` is a **data-minimization hint to the wallet**, not a verifier-side trust check. The verifier is still responsible for independently verifying issuer trust during validation. Do not treat this field as a substitute for that check.
## Holder Binding
```json
"require_cryptographic_holder_binding": false
```
This field defaults to `true`. Set it to `false` only when the credential genuinely does not require cryptographic key binding — cinema tickets, biometric-bound credentials (mDL portrait), or claims-bound diplomas.
There are real consequences when you set it to `false`:
- The Verifiable Presentation will not include a key binding proof, making the credential replayable.
- Nonce-based replay protection no longer applies. The spec requires `state` in the Authorization Request with at least 128 bits of entropy as a substitute.
## Format-Specific Gotchas — The `meta` Block
The `meta` object has a different shape for each credential format.
### SD-JWT VC (`dc+sd-jwt`)
```json
"meta": { "vct_values": ["https://issuer.demo.walt.id/draft13/EducationalID"] }
```
`vct_values` is an **array of strings**. The wallet matches if the credential's `vct` claim equals exactly one of the values listed.
### mdoc (`mso_mdoc`)
```json
"meta": { "doctype_value": "org.iso.18013.5.1.mDL" }
```
`doctype_value` is a **single string**, not an array. The wallet matches against the credential's MSO doctype.
### W3C VCDM (`jwt_vc_json`)
```json
"meta": {
"type_values": [
["VerifiableCredential", "UniversityDegreeCredential"]
]
}
```
`type_values` is an **array of arrays of strings**. The outer array is OR; the inner array is AND-over-types. So `[["VerifiableCredential", "UniversityDegreeCredential"]]` means "the credential's `type` array contains both of these values." This is easy to get wrong — a single-level array is a common mistake.
## Request credentials using DCQL with walt.id
The Community Stack verifier API accepts DCQL queries directly inside an OpenID4VP presentation request. Pass your `dcql_query` object in the request body when initiating a verification session.
- [Verify a Credential (OID4VP)](https://docs.walt.id/community-stack/verifier/credential-verification/vc-oid4vc.md) — Initiate a presentation request with a `dcql_query` using the community verifier API.
- [Verify an SD-JWT VC](https://docs.walt.id/community-stack/verifier/credential-verification/sd-jwt-vc-oid4vc.md) — SD-JWT VC-specific verification flow showing how DCQL claim paths map to disclosed fields.
## Related concepts
- [OpenID4VP](https://docs.walt.id/concepts/data-exchange-protocols/openid4vp.md) — The presentation protocol that carries the `dcql_query` inside an Authorization Request.
- [DC API](https://docs.walt.id/concepts/data-exchange-protocols/dc-api.md) — Browser-native credential presentation, also using `dcql_query` as its query format.
- [HAIP](https://docs.walt.id/concepts/data-exchange-protocols/haip.md) — The high-assurance profile that also uses DCQL and which is imporant for eIDAS 2.q
--------------------------------------------------------------------------------
PAGE: Trust Systems
URL: https://docs.walt.id/concepts/trust-systems
RAW: https://docs.walt.id/concepts/trust-systems.md
STACK: Concepts (stack-agnostic, applies to both Community and Enterprise Stack)
--------------------------------------------------------------------------------
# Trust Systems
Trust systems are the foundational infrastructure that enables secure, verifiable interactions in digital identity ecosystems. They answer the critical question: *"How do I know I can trust this entity?"*
## Why Trust Systems Matter
In decentralized identity ecosystems, parties that have never met must be able to:
- **Verify credentials** – Confirm that a credential was issued by a legitimate authority
- **Authenticate wallets** – Ensure wallet applications are genuine and secure
- **Establish authority** – Determine which entities are authorized to issue specific credentials
- **Enable interoperability** – Support cross-border and cross-jurisdictional recognition
Trust systems provide the technical and governance infrastructure to make these verifications possible at scale.
## Core Concepts
### Trust Anchors
A trust anchor is a cryptographic public key (and associated metadata) that serves as the root of trust for verifying other entities. Trust anchors are typically:
- Published in trusted lists or registries
- Managed by authoritative bodies (governments, standards organizations)
- Used to verify certificate chains and credential signatures
### Trust Lists
Trust lists are authoritative, machine-readable registries of trusted entities. They enable automated verification by providing:
- Public keys and identifiers for trusted entities
- Status information (active, suspended, revoked)
- Metadata about entity capabilities and authorizations
### Attestations
Attestations are cryptographic proofs that vouch for specific properties of an entity, such as:
- Wallet application integrity
- Key storage security
- Compliance with standards or regulations
## Trust Systems in This Section
This section covers three major trust system approaches used in digital identity:
### [EU Trust Lists](https://docs.walt.id/concepts/trust-systems/eu-trust-lists.md)
The European Union's standardized trust infrastructure for the EUDI Wallet ecosystem, based on ETSI standards and mandated by [eIDAS 2.0](https://walt.id/eidas2). Covers:
- National Trusted Lists and the EU List of Trusted Lists (LoTL)
- Trust anchors for Wallet Providers, PID Providers, and Attestation Providers
- ETSI TS 119 612 format and implementation
### [Trust in ISO mDL: VICALs and IACAs](https://docs.walt.id/concepts/trust-systems/iso-trust-vicals.md)
The trust infrastructure defined by ISO/IEC 18013-5 for mobile driver's licenses and mobile documents. Covers:
- Issuing Authority Certificate Authorities (IACAs)
- Verified Issuer Certificate Authority Lists ([VICALs](https://docs.walt.id/concepts/trust-systems/iso-trust-vicals.md))
- Reader authentication mechanisms
- Regional implementations (AAMVA, Austroads)
### [Wallet Attestations](https://docs.walt.id/concepts/trust-systems/wallet-attestations.md)
Cryptographic proofs that establish trust in digital identity wallets. Covers:
- Wallet Instance Attestations (WIA)
- Key Attestations and Wallet Trust Evidence (WTE)
- Integration with [OpenID4VCI](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md)
- Platform-specific attestation mechanisms
## Comparison of Trust Approaches
| Aspect | [EU Trust Lists](https://docs.walt.id/concepts/trust-systems/eu-trust-lists.md) | VICAL (ISO 18013-5) | [Wallet Attestations](https://docs.walt.id/concepts/trust-systems/wallet-attestations.md) |
|--------|---------------|---------------------|---------------------|
| **Primary Purpose** | Ecosystem-wide trust | [mDL](https://docs.walt.id/concepts/digital-credentials/mdoc-mdl-iso.md) issuer verification | Wallet integrity |
| **Scope** | EU Member States | Regional (per VICAL) | Per wallet instance |
| **Format** | XML (ETSI) | CBOR/COSE | JWT/COSE |
| **Governance** | European Commission | Regional bodies | Wallet Providers |
| **Update Frequency** | Periodic publication | Versioned refresh | Per-instance lifecycle |
## Further Reading
- [ID Ecosystems](https://docs.walt.id/concepts/id-ecosystems.md) – Overview of identity ecosystem components
- [Data Exchange Protocols](https://docs.walt.id/concepts/data-exchange-protocols.md) – How credentials are exchanged
- [Credential Lifecycle](../6.credential-lifecycle/) – Issuance, verification, and revocation
--------------------------------------------------------------------------------
PAGE: EU Trust Lists
URL: https://docs.walt.id/concepts/trust-systems/eu-trust-lists
RAW: https://docs.walt.id/concepts/trust-systems/eu-trust-lists.md
STACK: Concepts (stack-agnostic, applies to both Community and Enterprise Stack)
--------------------------------------------------------------------------------
# EU Trust Lists
EU Trust Lists are the backbone of trust infrastructure in the European Digital Identity ecosystem. They provide a standardized, machine-readable mechanism for establishing and verifying the trusted status of entities participating in digital identity transactions across the European Union.
## What Are EU Trust Lists?
EU Trust Lists are cryptographically signed, authoritative registries that contain trust anchors (public keys and identifiers) for entities authorized to participate in the European Digital Identity ecosystem. They enable wallets, relying parties, and other ecosystem components to automatically verify that an entity is supervised and trusted.
The trust list infrastructure operates at two levels:
| Level | Description | Managed By |
|-------|-------------|------------|
| **National Trusted Lists** | Country-specific lists of trusted entities | Individual Member States |
| **EU List of Trusted Lists (LoTL)** | Aggregated list linking all national trusted lists | European Commission |
The source of trust for the LoTL is the Official Journal of the European Union (OJEU), providing a legally binding foundation for the entire trust infrastructure.
## Entities Covered by Trust Lists
Under the [eIDAS 2.0](https://walt.id/eidas2) framework and the EUDI Wallet Architecture Reference Framework (ARF), trust lists contain trust anchors for:
- **Wallet Providers** – Organizations authorized to provide EUDI Wallet applications
- **PID Providers** – Issuers of Personal Identification Data (national identity credentials)
- **QEAA Providers** – Qualified Electronic Attestation of Attributes providers
- **PuB-EAA Providers** – Public Electronic Attestation of Attributes providers
- **Access Certificate Providers** – Issuers of certificates for relying party authentication
- **Registration Certificate Providers** – Issuers of certificates for entity registration
### Verification Process
When a wallet or relying party needs to verify an entity's trusted status:
1. **Retrieve the LoTL** – Download the EU List of Trusted Lists from the European Commission
2. **Locate the National TL** – Find the relevant Member State's trusted list
3. **Verify the Entity** – Check if the entity's trust anchor appears in the national list
4. **Validate Signatures** – Cryptographically verify the chain of signatures from the entity back to the LoTL
## Technical Standards
EU Trust Lists are governed by ETSI (European Telecommunications Standards Institute) specifications:
| Standard | Purpose |
|----------|---------|
| **ETSI TS 119 612** | Defines the format and structure of trusted lists |
| **ETSI TS 119 602** | Specifies trust list policies for the EUDI Wallet ecosystem |
| **ETSI EN 319 412** | Certificate profiles for trust service providers |
### Trust List Format
Trust lists are published in XML format with the following key components:
- **Scheme Information** – Metadata about the trust list (version, operator, territory)
- **Trust Service Provider List** – Entries for each trusted entity
- **Service Information** – Details about each trust service (type, status, certificates)
- **Digital Signature** – Cryptographic signature ensuring integrity and authenticity
## eIDAS 2.0 and Trust Lists
The revised eIDAS Regulation (EU) 2024/1183 significantly expands the role of trust lists:
### New Trust Service Types
eIDAS 2.0 introduces additional qualified trust services that must be registered in trust lists:
- Qualified electronic attestation of attributes (QEAA)
- Qualified electronic archiving services
- Qualified electronic ledger services
- Management of remote qualified electronic signature/seal creation devices
### Timeline for Implementation
| Date | Milestone |
|------|-----------|
| **April 2026** | Trust List v6 (TLv6) format enforcement |
| **April 2026** | Implementing Decision (EU) 2025/2164 applies |
| **2026-2027** | Full EUDI Wallet ecosystem deployment |
**Error:**
**Migration Required**: All stakeholders must ensure system compatibility with the new TLv6 format by April 28, 2026, to avoid validation failures.
## Benefits of EU Trust Lists
### For Relying Parties
- **Simplified Trust Decisions** – Single source of truth for entity verification
- **Cross-Border Recognition** – Automatic trust in entities from other Member States
- **Reduced Compliance Burden** – No need to maintain individual trust relationships
### For Trust Service Providers
- **Market Access** – Inclusion enables cross-border service provision
- **Legal Recognition** – Qualified status provides legal certainty
- **Interoperability** – Standardized format ensures compatibility
### For Citizens
- **Security Assurance** – Only verified entities can participate in the ecosystem
- **Privacy Protection** – Trusted entities must comply with data protection requirements
- **Cross-Border Usability** – Credentials work seamlessly across the EU
## Comparison with Other Trust Frameworks
| Aspect | EU Trust Lists | ICAO PKD | [VICAL](https://docs.walt.id/concepts/trust-systems/iso-trust-vicals.md) (ISO 18013-5) |
|--------|---------------|----------|---------------------|
| **Scope** | EU Member States | Global (passports) | Regional ([mDL](https://docs.walt.id/concepts/digital-credentials/mdoc-mdl-iso.md)) |
| **Governance** | European Commission | ICAO | National/Regional bodies |
| **Primary Use** | Digital identity ecosystem | Travel documents | Mobile driver's licenses |
| **Format** | XML (ETSI TS 119 612) | LDAP/X.509 | CBOR/COSE |
## Working with EU Trust Lists
### Accessing Trust Lists
Trust lists are publicly available through:
- **EU Digital Building Blocks Portal** – Official repository for all national trust lists
- **National Supervisory Bodies** – Direct access to country-specific lists
- **EUDI Wallet Reference Implementation** – Test environment for developers
## Further Reading
- [EU Trusted Lists Portal](https://digital-strategy.ec.europa.eu/en/policies/eu-trusted-lists)
- [ETSI Trust Service Providers](https://portal.etsi.org/TB-SiteMap/ESI/Trust-Service-Providers)
- [EUDI Wallet Architecture Reference Framework](https://eu-digital-identity-wallet.github.io/eudi-doc-architecture-and-reference-framework/)
- [eIDAS Regulation (EU) 2024/1183](https://eur-lex.europa.eu/eli/reg/2024/1183)
--------------------------------------------------------------------------------
PAGE: Trust in ISO mDL: VICALs and IACAs
URL: https://docs.walt.id/concepts/trust-systems/iso-trust-vicals
RAW: https://docs.walt.id/concepts/trust-systems/iso-trust-vicals.md
STACK: Concepts (stack-agnostic, applies to both Community and Enterprise Stack)
--------------------------------------------------------------------------------
# Trust in ISO mDL: VICALs and IACAs
The ISO/IEC 18013-5 standard for mobile driver's licenses ([mDLs](https://docs.walt.id/concepts/digital-credentials/mdoc-mdl-iso.md)) defines a comprehensive trust infrastructure that enables secure, cross-jurisdictional verification of digital credentials. At the heart of this infrastructure are Verified Issuer Certificate Authority Lists (VICALs) and Issuing Authority Certificate Authorities (IACAs).
## The Challenge of Distributed Trust
In digital identity ecosystems where multiple authorities independently issue credentials, establishing trust becomes complex. Consider a scenario where:
- Each state or province operates its own driver's license issuing authority
- Each authority maintains its own certificate infrastructure
- Verifiers (law enforcement, businesses, service providers) need to validate credentials from any jurisdiction
Without a centralized trust mechanism, verifiers would need to establish and maintain individual trust relationships with every issuing authority—an approach that doesn't scale.
## Issuing Authority Certificate Authority (IACA)
An IACA is the root X.509 certificate operated by an issuing authority for mobile documents. It forms the foundation of the trust chain for mDL credentials.
### IACA Characteristics
| Property | Description |
|----------|-------------|
| **Validity Period** | Up to 20 years |
| **Certificate Type** | Self-signed X.509 |
| **Purpose** | Signs Document Signer Certificates (DSCs) |
| **Verification** | Verified against the IACA's own public key |
### Trust Chain Structure
```
┌─────────────────────────────────────────────────────────────────┐
│ IACA (Root Certificate) │
│ Validity: Up to 20 years │
└─────────────────────────────────────────────────────────────────┘
│
│ Signs
▼
┌─────────────────────────────────────────────────────────────────┐
│ Document Signer Certificate (DSC) │
│ Validity: Shorter than IACA │
└─────────────────────────────────────────────────────────────────┘
│
│ Signs
▼
┌─────────────────────────────────────────────────────────────────┐
│ Mobile Security Object (MSO) │
│ Embedded in mDoc credential │
└─────────────────────────────────────────────────────────────────┘
```
When verifying an mDL, the verifier must:
1. Extract the Mobile Security Object (MSO) from the mDoc
2. Verify the MSO signature against the Document Signer Certificate
3. Verify the DSC signature against the IACA
4. Confirm the IACA is trusted (via VICAL or direct trust)
## Verified Issuer Certificate Authority List (VICAL)
A VICAL is a cryptographically signed, authoritative list of trusted IACAs that consolidates trust relationships into a single, manageable artifact.
### How VICAL Works
```
┌─────────────────────────────────────────────────────────────────┐
│ VICAL Provider │
│ (Trust Anchor) │
└─────────────────────────────────────────────────────────────────┘
│
│ Cryptographically signs
▼
┌─────────────────────────────────────────────────────────────────┐
│ VICAL │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ IACA Entry │ │ IACA Entry │ │ IACA Entry │ ... │
│ │ (State A) │ │ (State B) │ │ (State C) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
│ Distributed to
▼
┌─────────────────────────────────────────────────────────────────┐
│ Relying Parties │
│ (Law enforcement, businesses, services) │
└─────────────────────────────────────────────────────────────────┘
```
### VICAL Process Flow
| Step | Description |
|------|-------------|
| **Collection** | VICAL provider gathers public keys and metadata from recognized IACAs |
| **Validation** | Each IACA is validated and linked to specific credential types |
| **Signing** | The complete list is digitally signed by the VICAL provider |
| **Distribution** | Verifiers retrieve the signed VICAL via download or API |
| **Verification** | Verifiers check credentials against the VICAL |
### VICAL Technical Structure
VICALs use a signed, machine-readable format as defined in ISO/IEC 18013-5:
```
VICAL Structure (COSE_Sign):
├── Protected Header
│ └── Algorithm identifier
├── Unprotected Header
│ └── X.509 certificate chain
├── Payload
│ ├── Version identifier
│ ├── Provider name
│ ├── Issuance timestamp
│ ├── Expiration timestamp
│ ├── List identifier
│ └── IACA Records[]
│ ├── Issuer information
│ ├── Credential type(s) (docType)
│ ├── IACA public key
│ ├── Validity periods
│ └── Digital signatures
└── Signature
```
## Reader Authentication
ISO 18013-5 also defines mechanisms for authenticating verifiers (readers) to wallets, ensuring that sensitive credential data is only shared with legitimate parties.
### Reader Authentication Certificate
When a verifier requests credential data, they can provide a reader authentication certificate that proves their identity to the wallet. This enables:
- **Selective Disclosure** – Wallets can make informed decisions about what data to share
- **Audit Trails** – Credential holders can see who requested their data
- **Access Control** – Issuers can restrict which verifiers can access certain data elements
### ReaderAuth Structure
The `ReaderAuth` mechanism defined in ISO/IEC 18013-5 Section 9.1.4 includes:
- Reader certificate (attached via `x5chain` header per RFC 9360)
- Cryptographic signature proving possession of the private key
- Session transcript binding to prevent replay attacks
## Regional Implementations
### North America: AAMVA Digital Trust Service
The American Association of Motor Vehicle Administrators (AAMVA) operates the mDL Digital Trust Service (DTS) for North America:
| Component | Description |
|-----------|-------------|
| **VICAL** | Maintained list of all state-level IACAs |
| **Access** | Free download for relying parties |
| **Updates** | Regular refresh with versioning |
| **Compliance** | Adheres to ISO 18013-5 and AAMVA Implementation Guidelines |
### Australia: Austroads
Austroads coordinates the VICAL for Australian digital credentials, enabling cross-state verification of mobile driver's licenses.
### Alternative Approaches
Some regions have adopted different trust models:
- **New Zealand** – No centralized VICAL; exploring alternative frameworks
- **South Korea** – Blockchain-based trust management for mDLs
- **European Union** – EUDI Wallet ecosystem uses [EU Trust Lists](https://docs.walt.id/concepts/trust-systems/eu-trust-lists.md) (ETSI-based)
## Benefits of VICAL-Based Trust
### For Verifiers
- **Simplified Integration** – Single trust relationship with VICAL provider
- **Cross-Jurisdictional Support** – Verify credentials from any participating issuer
- **Automatic Updates** – Trust changes propagate through VICAL refresh
### For Issuing Authorities
- **Standardized Onboarding** – Clear process for VICAL inclusion
- **Broad Acceptance** – Credentials recognized by all VICAL-trusting verifiers
- **Key Rotation Support** – VICAL versioning handles certificate updates
### For Credential Holders
- **Wider Acceptance** – mDL works across jurisdictions
- **Privacy Protection** – Reader authentication enables informed consent
- **Interoperability** – Standard format ensures compatibility
## Comparison with Other Trust Models
| Aspect | VICAL (ISO 18013-5) | EU Trust Lists | OpenID Federation |
|--------|---------------------|----------------|-------------------|
| **Format** | CBOR/COSE | XML (ETSI) | JSON/JWT |
| **Scope** | Regional (mDL) | EU-wide | Global |
| **Update Model** | Versioned refresh | Periodic publication | Dynamic metadata |
| **Primary Use** | Mobile documents | Digital identity | Federated identity |
## Implementation Considerations
### For Verifier Applications
1. **VICAL Caching** – Cache the VICAL locally with appropriate refresh intervals
2. **Offline Support** – Ensure verification works without network connectivity
3. **Version Management** – Handle VICAL updates gracefully
4. **Certificate Validation** – Implement full chain validation including revocation checks
### For Issuing Authorities
1. **IACA Management** – Plan for certificate lifecycle (20-year validity)
2. **DSC Rotation** – Implement regular Document Signer Certificate rotation
3. **VICAL Participation** – Coordinate with regional VICAL providers
4. **Compliance** – Adhere to ISO 18013-5 and regional implementation guidelines
## Further Reading
- [ISO/IEC 18013-5:2021](https://www.iso.org/standard/69084.html) – Personal identification — ISO-compliant driving licence — Part 5: Mobile driving licence (mDL) application
- [AAMVA mDL Digital Trust Service](https://www.aamva.org/identity/mobile-driver-license-digital-trust-service)
- [AAMVA VICAL Download](https://vical.dts.aamva.org/)
- [ISO/IEC 18013-7](https://www.iso.org/standard/82772.html) – Mobile driving licence (mDL) add-on functions
--------------------------------------------------------------------------------
PAGE: Wallet Attestations
URL: https://docs.walt.id/concepts/trust-systems/wallet-attestations
RAW: https://docs.walt.id/concepts/trust-systems/wallet-attestations.md
STACK: Concepts (stack-agnostic, applies to both Community and Enterprise Stack)
--------------------------------------------------------------------------------
# Wallet Attestations
Wallet attestations are cryptographic proofs that establish trust in digital identity wallets. They enable credential issuers and relying parties to verify that they are communicating with legitimate, secure wallet applications before sharing sensitive identity data.
## Why Wallet Attestations Matter
In a digital identity ecosystem, trust flows in multiple directions:
- **Issuers** need assurance that credentials are being issued to secure, legitimate wallets
- **Verifiers (Relying Parties)** need confidence that presented credentials come from trustworthy wallet applications
- **Users** need protection against malicious apps impersonating legitimate wallets
Wallet attestations address these requirements by providing cryptographic proof of a wallet's integrity, security properties, and authorization status.
## OAuth 2.0 Attestation-Based Client Authentication
The foundation for wallet attestations in modern digital identity systems is the **OAuth 2.0 Attestation-Based Client Authentication** specification (IETF draft-ietf-oauth-attestation-based-client-auth). This standard extends OAuth 2.0 to enable traditionally "public" clients (like mobile wallet apps) to authenticate with authorization servers.
### The Problem with Traditional OAuth Clients
In classic OAuth 2.0, clients are categorized as:
| Client Type | Description | Authentication |
|-------------|-------------|----------------|
| **Confidential** | Server-side apps that can securely store secrets | Client secret or private key |
| **Public** | Mobile/browser apps that cannot securely store secrets | No authentication possible |
Digital identity wallets are inherently public clients—they run on user devices where secrets cannot be reliably protected. Yet issuers need strong assurance about which wallet is requesting credentials.
### The Attestation Solution
Attestation-based client authentication solves the public-client authentication problem by introducing a trusted third party (the Wallet Provider) who vouches for the wallet instance:
```
┌─────────────────────────────────────────────────────────────────┐
│ Wallet Provider │
│ (Trusted Authority) │
└─────────────────────────────────────────────────────────────────┘
│ │
│ (1) Validates wallet │ (3) Issues Client
│ integrity & security │ Attestation JWT
│ │
▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ Wallet Instance │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ (2) Generates key pair in secure hardware │ │
│ │ (4) Creates Proof of Possession (PoP) │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
│ (5) Client Attestation JWT + PoP
▼
┌─────────────────────────────────────────────────────────────────┐
│ Issuer / Verifier │
│ (Authorization Server) │
└─────────────────────────────────────────────────────────────────┘
```
### Two-Part Authentication
The attestation consists of two JWTs sent together, separated by a `~` character:
| Component | Created By | Purpose |
|-----------|------------|---------|
| **Client Attestation JWT** | Wallet Provider | Vouches for the wallet instance's authenticity and security |
| **Client Attestation PoP** | Wallet Instance | Proves possession of the key bound in the attestation |
**Token Request Example:**
```http
POST /token HTTP/1.1
Host: issuer.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code=SplxlOBeZQQYbYS6WxSbIA&
client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-client-attestation&
client_assertion=eyJhbGciOiJFUzI1NiIs...~eyJhbGciOiJFUzI1NiIs...
```
## How an Issuer Trusts a Wallet
When a wallet requests a credential from an issuer, the issuer must verify that:
1. The wallet is a legitimate, authorized application
2. The wallet meets security requirements for the credential type
3. The credential will be stored securely
### Issuer Trust Flow (OpenID4VCI)
```
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Wallet │ │ Wallet │ │ Issuer │
│ Instance │ │ Provider │ │ │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
│ 1. Generate key pair │ │
│ in secure hardware │ │
│ │ │
│ 2. Request attestation │ │
│ (with platform evidence) │ │
│──────────────────────────────>│ │
│ │ │
│ │ 3. Validate: │
│ │ - App integrity │
│ │ - Device security │
│ │ - Key storage type │
│ │ │
│ 4. Client Attestation JWT │ │
│<──────────────────────────────│ │
│ │ │
│ 5. Discover issuer metadata │
│──────────────────────────────────────────────────────────────>│
│ │ │
│ 6. Authorization request (PAR) │
│ + Client Attestation + PoP │
│──────────────────────────────────────────────────────────────>│
│ │ │
│ │ 7. Validate attestation: │
│ │ - Signature (Provider) │
│ │ - PoP (Wallet key) │
│ │ - Provider in Trust List│
│ │ - Security claims │
│ │ │
│ 8. Authorization response │
│<──────────────────────────────────────────────────────────────│
│ │ │
│ 9. Token request + attestation │
│──────────────────────────────────────────────────────────────>│
│ │ │
│ 10. Access token │
│<──────────────────────────────────────────────────────────────│
│ │ │
│ 11. Credential request │
│ + Key proof (+ optional WTE) │
│──────────────────────────────────────────────────────────────>│
│ │ │
│ │ 12. Verify key binding │
│ │ Issue credential │
│ │ │
│ 13. Credential │
│<──────────────────────────────────────────────────────────────│
```
### What the Issuer Validates
| Check | Purpose |
|-------|---------|
| **Attestation Signature** | Confirms the Wallet Provider issued this attestation |
| **Proof of Possession** | Confirms the wallet controls the attested key |
| **Provider Trust** | Verifies the Wallet Provider is in the [EU Trust List](https://docs.walt.id/concepts/trust-systems/eu-trust-lists.md) |
| **Expiration** | Ensures the attestation is still valid |
| **Security Claims** | Checks key storage type, user authentication requirements |
| **Revocation Status** | Confirms the wallet instance hasn't been revoked |
### Client Attestation JWT Claims
```json
{
"typ": "wallet-attestation+jwt",
"alg": "ES256",
"kid": "wallet-provider-key-1"
}
.
{
"iss": "https://wallet-provider.example.com",
"sub": "https://wallet-provider.example.com/wallet/v1.6.0",
"iat": 1704067200,
"exp": 1704153600,
"wallet_name": "Example EUDI Wallet",
"wallet_version": "1.6.0",
"key_type": "STRONGBOX",
"user_authentication": "SYSTEM_BIOMETRIC",
"attested_security_context": "https://trust-list.eu/asc/high",
"cnf": {
"jwk": {
"kty": "EC",
"crv": "P-256",
"x": "TCAER19Zvu3OHF4j4W4vfSVoHIP1ILilDls7vCeGemc",
"y": "ZxjiWWbZMQGHVWKVQ4hbSIirsVfuecCE6t4jT9F2HZQ"
}
}
}
```
### Wallet Trust Evidence (WTE)
For high-assurance credentials, issuers may require additional proof about the key that will hold the credential. **Wallet Trust Evidence** is conveyed at the Credential Endpoint:
```json
{
"format": "dc+sd-jwt",
"credential_identifier": "eu.europa.ec.eudi.pid",
"proofs": {
"jwt": {
"proof_type": "jwt",
"jwt": "eyJhbGciOiJFUzI1NiIs..."
},
"attestation": {
"attestation": "eyJhbGciOiJFUzI1NiIs...",
"proof": "eyJhbGciOiJFUzI1NiIs..."
}
}
}
```
The WTE attestation includes claims about:
| Claim | Description |
|-------|-------------|
| `key_type` | Hardware security level (e.g., "strong_box", "tee", "software") |
| `user_authentication` | Required user auth (e.g., "biometric", "pin", "none") |
| `device_keys` | Information about device-bound keys |
| `cnf` | The public key that will be bound to the credential |
## How a Verifier Trusts a Wallet
When a wallet presents credentials to a verifier (relying party), the trust requirements differ from issuance. The verifier primarily needs to trust the **credential** and its **issuer**, but may also want assurance about the wallet.
### Verifier Trust Flow (OpenID4VP)
```
┌──────────┐ ┌──────────┐
│ Wallet │ │ Verifier │
│ Instance │ │ (RP) │
└────┬─────┘ └────┬─────┘
│ │
│ 1. Authorization Request │
│ (presentation_definition) │
│<──────────────────────────────────────────────────│
│ │
│ 2. User consent │
│ │
│ 3. Create Verifiable Presentation │
│ - Select credentials │
│ - Apply selective disclosure │
│ - Sign with holder key │
│ │
│ 4. Authorization Response │
│ (vp_token + optional wallet attestation) │
│──────────────────────────────────────────────────>│
│ │
│ 5. Validate: │
│ - Credential signatures │
│ - Issuer trust (Trust List)│
│ - Holder binding │
│ - Credential status │
│ - (Optional) Wallet attestation│
│ │
│ 6. Access granted │
│<──────────────────────────────────────────────────│
```
### What the Verifier Validates
| Check | Primary Trust Source | Purpose |
|-------|---------------------|---------|
| **Credential Signature** | Issuer's key | Confirms credential authenticity |
| **Issuer Trust** | EU Trust List | Verifies issuer is authorized |
| **Holder Binding** | Presentation signature | Confirms presenter controls the credential |
| **Credential Status** | Status list | Checks for revocation/suspension |
| **Wallet Attestation** | Wallet Provider | (Optional) Additional assurance about wallet security |
### When Verifiers Require Wallet Attestation
Wallet attestation during presentation is **optional** but may be required for:
| Scenario | Reason |
|----------|--------|
| **High-value transactions** | Additional security assurance |
| **Regulated industries** | Compliance requirements |
| **Government services** | Policy mandates |
| **Sensitive data access** | Risk mitigation |
### Verifier Attestation (Reverse Trust)
[OpenID4VP](https://docs.walt.id/concepts/data-exchange-protocols/openid4vp.md) also supports **Verifier Attestation**, allowing wallets to verify the legitimacy of verifiers before sharing credentials:
```json
{
"client_id": "https://verifier.example.com",
"client_id_scheme": "verifier_attestation",
"verifier_attestation": "eyJhbGciOiJFUzI1NiIs..."
}
```
This enables wallets to:
- Confirm the verifier is a legitimate organization
- Check the verifier's authorization to request specific credentials
- Display verified verifier information to users
## Types of Wallet Attestations
### Wallet Instance Attestation (WIA)
The WIA attests to the wallet application's integrity and authorization:
| Claim | Description |
|-------|-------------|
| `iss` | Wallet Provider identifier |
| `sub` | Wallet instance/solution identifier |
| `wallet_name` | Human-readable wallet name |
| `wallet_version` | Application version |
| `client_status` | Current status (active, suspended, revoked) |
| `cnf` | Bound public key |
### Key Attestation
Proves properties of the cryptographic keys:
| Property | Description |
|----------|-------------|
| `key_type` | Storage type (STRONGBOX, TEE, SOFTWARE) |
| `user_authentication` | Required authentication (BIOMETRIC, PIN, NONE) |
| `key_storage_status` | Key validity and revocation info |
### Attestation Lifecycle Models
| Model | Description | Use Case |
|-------|-------------|----------|
| **Long-lived** | Reused across transactions; requires revocation checking | Reduced latency; simpler wallet implementation |
| **Ephemeral** | Single-use per transaction; no revocation needed | Maximum privacy; no tracking across verifiers |
## Integration with OpenID Protocols
### OpenID4VCI (Credential Issuance)
Wallet attestation is used at multiple points:
| Endpoint | Attestation Use |
|----------|-----------------|
| **PAR Endpoint** | Client authentication via attestation |
| **Token Endpoint** | Client authentication via attestation |
| **Credential Endpoint** | Key attestation / WTE for credential binding |
### OpenID4VP (Credential Presentation)
| Component | Attestation Use |
|-----------|-----------------|
| **Authorization Request** | Verifier attestation (optional) |
| **Authorization Response** | Wallet attestation (optional) |
## Privacy Considerations
Wallet attestations must be designed to protect user privacy:
| Requirement | Implementation |
|-------------|----------------|
| **No unique identifiers** | Attestations must not contain values that enable user tracking |
| **Unlinkability** | Different attestations should be used with different verifiers |
| **Minimal disclosure** | Only necessary claims should be included |
| **Short validity** | Limits the window for correlation attacks |
**Error:**
**Privacy Risk**: Using the same wallet attestation across multiple verifiers could enable correlation of user activity. Implementations should consider using ephemeral attestations or rotating attestations per verifier.
## Regulatory Framework
Wallet attestations are mandated by the [eIDAS 2.0](https://walt.id/eidas2) framework:
| Regulation | Requirement |
|------------|-------------|
| **eIDAS 2.0** | Wallet Providers must ensure wallet integrity and security |
| **CIR 2024/2977** | Technical requirements for wallet attestations |
| **CIR 2024/2979** | Certification requirements for wallet solutions |
| **ETSI 119 476-3** | Technical specification for WUA and WIA (in development) |
### Level of Assurance
| LoA | Key Storage | User Authentication | Attestation Requirements |
|-----|-------------|---------------------|-------------------------|
| **Low** | Software | None required | Basic WIA |
| **Substantial** | Hardware recommended | PIN or biometric | WIA + Key attestation |
| **High** | Hardware required | Strong authentication | WIA + WTE + certified WSCD |
## Further Reading
- [OAuth 2.0 Attestation-Based Client Authentication](https://datatracker.ietf.org/doc/draft-ietf-oauth-attestation-based-client-auth/)
- [OpenID for Verifiable Credential Issuance](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html)
- [OpenID for Verifiable Presentations](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html)
- [EUDI Wallet Architecture Reference Framework](https://eu-digital-identity-wallet.github.io/eudi-doc-architecture-and-reference-framework/)
- [ETSI TS 119 476](https://www.etsi.org/deliver/etsi_ts/119400_119499/119476/) – Electronic Signatures and Trust Infrastructures
################################################################################
# PART 2 OF 3: COMMUNITY STACK — OPEN SOURCE (205 pages)
# The Community Stack is walt.id's FREE, OPEN-SOURCE product line: standalone
# Issuer API, Verifier API, and Wallet API services plus Kotlin multi-platform
# libraries. Single-tenant, self-hosted (Docker), no license required.
# Do NOT confuse with the commercial Enterprise Stack.
################################################################################
--------------------------------------------------------------------------------
PAGE: Getting Started
URL: https://docs.walt.id/community-stack/home
RAW: https://docs.walt.id/community-stack/home.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Getting Started
Explore our issuer, verifier and wallet product below or learn more about the walt.id Community Stack and its
architecture [here](https://docs.walt.id/community-stack/home/the-community-stack.md).
**Note:**
New to walt.id? Start with **Issuer2**, **Verifier2**, and **Wallet API v2** below — they implement the finalized [**OID4VCI 1.0**](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md) and [**OID4VP 1.0**](https://docs.walt.id/concepts/data-exchange-protocols/openid4vp.md) specifications, instead of the draft versions supported by the original APIs. The original Issuer, Verifier, and Wallet APIs are deprecated but remain available for existing integrations.
Video: https://youtu.be/KINBsck7Jzg
#### [Issuer](https://docs.walt.id/community-stack/issuer2/getting-started.md)
- [API](https://docs.walt.id/community-stack/issuer2/getting-started.md) - Issue credentials via OID4VCI 1.0 using the Issuer2 API
- [Apps](https://docs.walt.id/community-stack/issuer2/apps/portal2/overview.md) - Portal2: issue and verify credentials via a white-label UI.
#### [Verifier](https://docs.walt.id/community-stack/verifier2/getting-started.md)
- [API](https://docs.walt.id/community-stack/verifier2/getting-started.md) - Request and verify credentials from holders via OID4VP 1.0 using the Verifier2 API.
- [Apps](https://docs.walt.id/community-stack/verifier2/apps/portal2/overview.md) - Portal2: issue and verify credentials via a white-label UI.
#### [Wallet](https://docs.walt.id/community-stack/wallet2/getting-started.md)
- [API](https://docs.walt.id/community-stack/wallet2/getting-started.md) - Create and manage ID wallets via the Wallet API v2 (custodial), with OID4VCI 1.0 and OID4VP 1.0 support.
- [Apps](https://docs.walt.id/community-stack/mobile-wallet/getting-started.md) - Native Android/iOS wallet SDK (Kotlin Multiplatform + Swift), with OID4VCI 1.0 and OID4VP 1.0 support.
---
### Quick Start
If you want to try out all the walt.id services quickly, you can use
our [docker compose](https://github.com/walt-id/waltid-identity/tree/main/docker-compose) shown below. This launches
locally both service generations:
- **Current generation:** [Issuer API v2](https://github.com/walt-id/waltid-identity/tree/main/waltid-services/waltid-issuer-api2), [Verifier API v2](https://github.com/walt-id/waltid-identity/tree/main/waltid-services/waltid-verifier-api2), [Wallet API v2](https://github.com/walt-id/waltid-identity/tree/main/waltid-services/waltid-wallet-api2)
- **Original:** [Issuer API](https://github.com/walt-id/waltid-identity/tree/main/waltid-services/waltid-issuer-api), [Verifier API](https://github.com/walt-id/waltid-identity/tree/main/waltid-services/waltid-verifier-api), [Wallet API](https://github.com/walt-id/waltid-identity/tree/main/waltid-services/waltid-wallet-api)
The docker compose setup also launches our [Web Wallet](https://github.com/walt-id/waltid-identity/tree/main/waltid-applications/waltid-web-wallet) _(v1 only)_, [Web Portal v1](https://github.com/walt-id/waltid-identity/tree/main/waltid-applications/waltid-web-portal) _(v1 only)_, [Web Portal v2](https://docs.walt.id/community-stack/issuer2/apps/portal2/overview.md), and [Verifiable Credentials Repo](https://github.com/walt-id/waltid-credentials).
We also provide links to all services deployed in our test environment below.
**Clone [walt.id identity](https://github.com/walt-id/waltid-identity)**
```bash
git clone https://github.com/walt-id/waltid-identity.git && cd waltid-identity
```
**Launch the services**
```bash
cd docker-compose && docker compose up
```
#### Access Services
Click the link(s) below to open the related services. Local environment link only works if you executed the command
above.
**APIs — current generation** _(recommended for new projects — OID4VCI 1.0 / OID4VP 1.0)_
- **Issuer2 API** - [Deployed](https://issuer2.demo.walt.id/swagger/index.html) | [Local](http://localhost:7005/)
- **Verifier2 API** - [Deployed](https://verifier2.demo.walt.id/swagger/index.html) | [Local](http://localhost:7004/)
- **Wallet API v2** - [Local](http://localhost:7006/swagger)
**APIs — original** _(deprecated — draft OID4VCI / OID4VP versions)_
- **Issuer API** - [Deployed](https://issuer.demo.walt.id/swagger/index.html) | [Local](http://localhost:7002/)
- **Verifier API** - [Deployed](https://verifier.demo.walt.id/swagger/index.html) | [Local](http://localhost:7003/)
- **Wallet API** - [Deployed](https://wallet.walt.id/swagger/index.html#/) | [Local](http://localhost:7001/)
**Apps**
- **Web Wallet** _(v1 only)_ - [Deployed](https://wallet.demo.walt.id/login) | [Local](http://localhost:7101/)
- **Web Portal** _(v1 only)_ - [Deployed](https://portal.walt.id/) | [Local](http://localhost:7102/)
- **[Web Portal v2](https://docs.walt.id/community-stack/issuer2/apps/portal2/overview.md)** - [Deployed](https://portal2.demo.walt.id) | [Local](http://localhost:7105/)
- **Verifiable Credential Repo** - [Deployed](https://credentials.walt.id/) | [Local](http://localhost:7103/)
### Guides
- [Tutorial (30 min)](https://docs.walt.id/community-stack/home/tutorial-30-min-v2.md) - End-to-end ID use-case using Issuer2, Verifier2, and Wallet API v2.
- [FAQ](https://docs.walt.id/community-stack/home/faq.md) - Frequently asked questions about the Community Stack.
--------------------------------------------------------------------------------
PAGE: Quickstart (5 min)
URL: https://docs.walt.id/community-stack/home/quickstart-5-min
RAW: https://docs.walt.id/community-stack/home/quickstart-5-min.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Quickstart (5 min)
Video: https://youtu.be/fufhtqb_yjQ?si=zKkQSr20BpgX4Brt
In this quickstart guide, we will get ourselves a digital ID wallet, issue a credential into the wallet, and then present that credential to a verifier.
**Note:**
This quickstart is built on the original **Issuer**, **Verifier**, and **Wallet** (v1), since they come with a ready-to-use wallet UI. **Wallet API v2** does not yet have an equivalent hosted wallet UI — a v2 wallet UI is coming soon. If you want to try the latest **Issuer2**, **Verifier2**, and **Wallet API v2** instead, check out the [Tutorial v2 (30 min)](https://docs.walt.id/community-stack/home/tutorial-30-min-v2.md), which walks through an end-to-end digital identity use case using the APIs directly. In the meantime, to try v2 with a wallet UI, grab the walt.id Mobile Wallet Android APK from the [Assets section of our v1.0.0 release](https://github.com/walt-id/waltid-identity/releases/tag/v1.0.0), or use the EUDI Wallet Reference Implementation. The iOS build for the walt.id Mobile Wallet is available via TestFlight — [reach out to us](https://walt.id/contact) for access.
### The Components
- [**The Wallet**](https://wallet.demo.walt.id/){:target="_blank"}: This will utilize a demo version of the walt.id web wallet.
- [**The Issuer**](https://portal.demo.walt.id/){:target="_blank"}: This will utilize a demo version of the walt.id web portal.
- [**The Verifier**](https://portal.demo.walt.id/){:target="_blank"}: This will also utilize a demo version of the walt.id web portal.
## 1. Create Wallet
Visit the walt.id web-wallet [here](https://wallet.demo.walt.id){:target="_blank"} and [register a new account](https://wallet.demo.walt.id/signup){:target="_blank"} using your email and password.
## 2. Issue Credential
To issue a credential using the [walt.id web portal](https://portal.demo.walt.id/){:target="_blank"}, follow these steps:
1. Access the Portal: Visit the walt.id web portal [here](https://portal.demo.walt.id/){:target="_blank"} and select the credential you wish to issue, such as `BankID`. Click "Start" located at the top right corner.
2. (optional) Configure Credential Issuance: On the next page, you can configure the credential issuance settings. You have the option to:
- Choose the credential format, such as [W3C (JWT/SD-JWT)](https://docs.walt.id/concepts/digital-credentials/verifiable-credentials-w3c.md){:target="_blank"}, [IETF SD-JWT VC](https://docs.walt.id/concepts/digital-credentials/sd-jwt-vc.md){:target="_blank"}, or others.
- Specify the [DID](https://docs.walt.id/concepts/decentralised-identifiers.md){:target="_blank"} method to be used as the issuer DID.
- Under "Security Settings," select the authentication method. By default, it is set to "pre-authorized," meaning the user is already authenticated when receiving the credential offer.
3. Issue the Credential: Use the configured settings or leave the default credential issuance settings and click "Issue." This will generate an [**OID4VCI**](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md) credential offer using the issuer API in the background.
4. Receive the Credential Offer: The OID4VCI offer will be displayed on the next screen as a QR code and also provided as a direct link behind the "Open Web Wallet" button.
- The QR code is ideal if the wallet is on a mobile device, as the device can easily scan the code.
- The direct link is useful if the receiving device also has the wallet installed.
5. Open the Credential offer in [Web Wallet](https://wallet.demo.walt.id/){:target="_blank"}: Click "Open Web Wallet" to view the offer in the web wallet.
## 3. Receive Credential
Accept the credential offer displayed in the wallet. You should now have the credential in your wallet.
## 4. Verify Credential
To verify the credential in your wallet using the [walt.id web portal](https://portal.demo.walt.id/){:target="_blank"}, follow these steps:
1. Access the Portal: Visit the walt.id web portal [here](https://portal.demo.walt.id/){:target="_blank"} and select the credential you issued in the previous step to verify, such as `BankID`. Click "Start" located at the top right corner.
2. Switch to the 'Verify' tab at the top below the smiley.
3. (optional) Configure Credential Verification with the following options:
- Choose the credential format, such as [W3C (JWT/SD-JWT)](https://docs.walt.id/concepts/digital-credentials/verifiable-credentials-w3c.md){:target="_blank"}, [IETF SD-JWT VC](https://docs.walt.id/concepts/digital-credentials/sd-jwt-vc.md){:target="_blank"}, or others.
- Specify the [DID](https://docs.walt.id/concepts/decentralised-identifiers.md){:target="_blank"} method
- Under "Credential Policies", select the [policies](https://docs.walt.id/community-stack/verifier/credential-verification/policies/overview.md){:target="_blank"} which should be applied against the credential.
4. Verify the Credential: Use the configured settings or leave the default credential verification settings and click "Verify." This will generate an [**OID4VP**](https://docs.walt.id/concepts/data-exchange-protocols/openid4vp.md) authorization request using the verifier API in the background.
5. Receive Credential Authorization Request: The OID4VP authorization URL will be displayed on the next screen as a QR code and also provided as a direct link behind the "Open Web Wallet" button.
- The QR code is ideal if the wallet is on a mobile device, as the device can easily scan the code.
- The direct link is useful if the receiving device also has the wallet installed.
6. Open the Credential authorization request in [Web Wallet](https://wallet.demo.walt.id/){:target="_blank"}: Click "Open Web Wallet" to view the verification request in the web wallet.
## 5. Present Credential
Accept the verification and inspect the verification results on the page you get redirected to.
## Next Steps
**Note:**
If you enjoy our tools, please leave us a star ⭐ [on GitHub](https://github.com/walt-id/waltid-identity).
[Checkout our tutorial](https://docs.walt.id/community-stack/home/tutorial-30-min.md) for an end-to-end use-case using our APIs (Issuer, Verifier and Wallet).
--------------------------------------------------------------------------------
PAGE: Tutorial (30 min)
URL: https://docs.walt.id/community-stack/home/tutorial-30-min
RAW: https://docs.walt.id/community-stack/home/tutorial-30-min.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Tutorial (30 min)
Video: https://youtu.be/1Qbj2kVMee8
**Note:**
Starting a new integration? The [v2 tutorial](https://docs.walt.id/community-stack/home/tutorial-30-min-v2.md) covers the same walkthrough
using Issuer2, Wallet2, and Verifier2 which fully support [**OID4VCI**](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md) V1 and [**OID4VP**](https://docs.walt.id/concepts/data-exchange-protocols/openid4vp.md) V1. This page remains for the original Issuer, Wallet,
and Verifier (v1) that support draft versions of the OID4VCI and OID4VP protocols.
In this 30 min tutorial, we will take a look at the issuer, verifier, and wallet API of
the walt.id Community Stack. If you want to try it out via a UI first, please go [here](https://docs.walt.id/community-stack/home/quickstart-5-min.md).
The tutorial will be split into the following sections:
1. Create a wallet
2. Issue a credential into the wallet
3. Request & verify the credential from the wallet
With that, we are covering a regular digital ID use-case end-to-end using the walt.id API
components:
- [Issuer API](https://docs.walt.id/community-stack/issuer/getting-started.md)
- [Verifier API](https://docs.walt.id/community-stack/verifier/getting-started.md)
- [Wallet API](https://docs.walt.id/community-stack/wallet/getting-started.md)
### The Credential
For this tutorial, we will be issuing ourselves a University Degree Credential in
the [W3C credential format](https://docs.walt.id/concepts/digital-credentials/verifiable-credentials-w3c.md)
using a JSON Web Token (JWT) signature.
**Note:**
If you are curious to learn more about other types of credential formats, please
go [here](https://docs.walt.id/concepts/digital-credentials.md).
#### Example University Degree Credential
```json
{
// The contextual information required for data integrity and 1.api
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://www.w3.org/2018/credentials/examples/v1"
],
"id": "http://example.gov/credentials/3732",
// The credential's unique identifier
"type": [
"VerifiableCredential",
"UniversityDegree"
// Specifies the kind of credential being issued
],
"issuer": {
"id": "did:web:vc.transmute.world"
// The issuer's unique identifier (DID)
},
"issuanceDate": "2020-03-10T04:24:12.164Z",
// When the credential was issued
"credentialSubject": {
// Information about the credential's recipient and the degree earned
"id": "did:example:ebfeb1f712ebc6f1c276e12ec21",
"degree": {
"type": "BachelorDegree",
"name": "Bachelor of Science and Arts"
}
}
}
```
## Setup
To bring up
the [issuer](https://docs.walt.id/community-stack/issuer/getting-started.md), [wallet](https://docs.walt.id/community-stack/wallet/getting-started.md)
and [verifier API](https://docs.walt.id/community-stack/verifier/getting-started.md) locally, we will be using
our [docker compose](https://github.com/walt-id/waltid-identity/tree/main/docker-compose).
**Clone [walt.id identity](https://github.com/walt-id/waltid-identity)**
```bash
git clone https://github.com/walt-id/waltid-identity.git && cd waltid-identity
```
**Launch the services**
```bash
cd docker-compose && docker compose up
```
#### Access APIs
- **Issuer API** - [http://localhost:7002/](http://localhost:7002/)
- **Verifier API** - [http://localhost:7003/](http://localhost:7003/)
- **Wallet API** - [http://localhost:7001/](http://localhost:7001/)
## Create a Wallet
To create our wallet, we will use the [walt.id wallet API](https://docs.walt.id/community-stack/wallet/getting-started.md). This API will
be managed by the operator, which means that the
wallet we create will be a custodial wallet. In a custodial wallet, the keys are controlled by the operator of the
software, unlike in a non-custodial wallet where the user has full control over their keys.
### Create Wallet Account
**Option: CURL**
[API Reference](https://wallet.demo.walt.id/swagger/index.html#/Authentication/post_wallet_api_auth_create)
Each created account receives a default "did:key" and private key (Ed25519).
```bash
curl -X 'POST' \
'http://0.0.0.0:7001/wallet-api/auth/register' \
-H 'accept: */*' \
-H 'Content-Type: application/json' \
-d '{
"type": "email",
"name": "Test",
"email": "test@email.com",
"password": "test"
}'
```
**Body Parameters**
```json
{
"name": "string",
"email": "string",
"password": "string",
"type": "{type}"
}
```
- `name`: _[string]_ - The name of the user.
- `email`: _[string]_ - The email of the user.
- `password`: _[string]_ - The password of the user.
- `type`: _[string]_ - The type of the user account. `email` or `address` (for web3 login).
### Login to Wallet Account
**Option: CURL**
[API Reference](https://wallet.demo.walt.id/swagger/index.html#/Authentication/post_wallet_api_auth_login)
```bash
curl -X 'POST' \
'http://0.0.0.0:7001/wallet-api/auth/login' \
-H 'accept: */*' \
-H 'Content-Type: application/json' \
-d '{
"type": "email",
"email": "test@email.com",
"password": "test"
}'
```
**Body Parameters**
```json
{
"type": "{type}",
"email": "string",
"password": "string"
}
```
- `type`: _[string]_ - The type of the user account. `email` or `address` (for web3 login).
- `email`: _[string]_ - The email of the user.
- `password`: _[string]_ - The password of the user.
**Example Response**
Now a session is automatically created for cookie-based authentication. For Bearer Token Authentication, the `token`
returned
must be provided in the header for each request that needs authentication. Refer to
the [overview section](https://docs.walt.id/community-stack/wallet/authentication/overview.md#cookie-based-authentication) for more details.
```json
{
"token": "KL-a_dk1qO8moCX4gxaGfb7_TS8RK-JWVKZk9BBP0-s",
"id": "018045e5-942c-4362-b535-658c4dd581ef",
"username": "user@email.com"
}
```
### Retrieve Wallet ID
As there could be support for multiple wallets per account in the future. We will need to retrieve our
wallet ID for the one wallet that got created with the following call. We should save this
ID for future reference.
**Option: CURL**
[API Reference](https://wallet.demo.walt.id/swagger/index.html#/Accounts/get_wallet_api_wallet_accounts_wallets)
```bash
curl -X 'GET' \
'http://0.0.0.0:7001/wallet-api/wallet/accounts/wallets' \
-H 'accept: application/json' \
-H 'authorization: Bearer {token}'
```
**Header Parameters**
- `authorization`: _String_ - Bearer token obtained from the [login step](#login-to-wallet-account). Format: `Bearer {token}`.
**Example Response**
```json
{
"account": "108753e3-5ab4-4096-8699-ad0653c79a09",
"wallets": [
{
// ⬇️ Save for later ⬇️
"id": "4d7ef7c6-a76b-409b-b042-68d15a484082",
// ⬆️ Save for later ⬆️
"name": "Wallet of Test",
"createdOn": "2025-04-16T09:19:11.585Z",
"addedOn": "2025-04-16T09:19:11.597Z",
"permission": "ADMINISTRATE"
}
]
}
```
**Note:**
Make sure to save the ID for future reference.
Now that we have created the wallet account and have the wallet ID, we can proceed to the issuer to create a credential
offer. This offer can then be received in the wallet we just set up. Additionally, it's important to note that each
wallet is
automatically assigned a set of keys and a did:jwk. This feature can be disabled if desired through the [wallet
configuration settings](/community-stack/wallet/configurations/config-files/features).
## Issue Credential
### Create Credential Offer
**Option: CURL**
**Endpoint:
** `/openid4vc/jwt/issue` | [API Reference](https://issuer.demo.walt.id/swagger/index.html#/Credential%20Issuance/post_openid4vc_jwt_issue)
**Example Request**
```bash
curl -X 'POST' \
'http://localhost:7002/openid4vc/jwt/issue' \
-H 'accept: text/plain' \
-H 'Content-Type: application/json' \
-d '{
"issuerKey": {
"type": "jwk",
"jwk": {
"kty": "OKP",
"d": "JvJIpga2GD8LJeRu4Sv-mL4thE31DuFlr9PA04CIoZY",
"crv": "Ed25519",
"kid": "iJMS5bkZVIlncfq_Lf_SuxJ2JtQ5Hvaz7tWPnAjUUds",
"x": "FZdvwC8aGhRwqzWptej0NZgtwYAI1SyFg1mKDETOfqE"
}
},
"issuerDid": "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoiaUpNUzVia1pWSWxuY2ZxX0xmX1N1eEoySnRRNUh2YXo3dFdQbkFqVVVkcyIsIngiOiJGWmR2d0M4YUdoUndxeldwdGVqME5aZ3R3WUFJMVN5RmcxbUtERVRPZnFFIn0",
"credentialConfigurationId": "UniversityDegree_jwt_vc_json",
"credentialData": {
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://www.w3.org/2018/credentials/examples/v1"
],
"id": "http://example.gov/credentials/3732",
"type": [
"VerifiableCredential",
"UniversityDegree"
],
"issuer": {
"id": "did:web:vc.transmute.world"
},
"issuanceDate": "2020-03-10T04:24:12.164Z",
"credentialSubject": {
"id": "did:example:ebfeb1f712ebc6f1c276e12ec21",
"degree": {
"type": "BachelorDegree",
"name": "Bachelor of Science and Arts"
}
}
},
"mapping": {
"id": "",
"issuer": {
"id": ""
},
"credentialSubject": {
"id": ""
},
"issuanceDate": "",
"expirationDate": ""
},
"authenticationMethod": "PRE_AUTHORIZED",
"standardVersion": "DRAFT13"
}'
```
**Header Parameters**
- `statusCallbackUri`: _URL_ - Receive updates on the created issuance process, e.g. when a credential was successfully
claimed. The parameter expects a URL which can accept a JSON POST request. The URL can also hold a `$id`, which will
be
replaced by the issuance session id. For example: `https://myurl.com/$id`, `https://myurl.com`
or `https://myurl.com/test/$id`
Expand To Learn More
**Body**
The data send to the provided URL will contain a JSON body:
- `id` : _String_ - the issuance session id
- `type`: _String_ - the event type
- `data`: _JsonObject_ - the data for the event
**Event Types**
Possible events (event types) and their data are:
- `resolved_credential_offer` with the credential offer as JSON (in our Web Wallet: called when the issuance offer
is
entered into the wallet, but not processing / accepted yet)
- `requested_token` with the issuance request for the token as json object (called for the token required to receive
the
credentials)
Credential issuance (called for every credential that's issued (= requested from wallet))
- `jwt_issue` with `jwt` being the issued jwt
- `sdjwt_issue` with `sdjwt` being the issued sdjwt
- `batch_jwt_issue` with `jwt` being the issued jwt
- `batch_sdjwt_issue` with `sdjwt` being the issued sdjwt
- `generated_mdoc` with `mdoc` being the CBOR (HEX) of the signed mdoc
To allow for secure business logic flows, if a callback URL is set, and it cannot be reached, the issuance will not
commence further (after that point). If no callback URL is set, the issuance logic does not change in any way.
**Body**
```json
{
"issuerKey": {
"type": "jwk",
"jwk": {
"kty": "OKP",
"d": "JvJIpga2GD8LJeRu4Sv-mL4thE31DuFlr9PA04CIoZY",
"crv": "Ed25519",
"kid": "iJMS5bkZVIlncfq_Lf_SuxJ2JtQ5Hvaz7tWPnAjUUds",
"x": "FZdvwC8aGhRwqzWptej0NZgtwYAI1SyFg1mKDETOfqE"
}
},
"issuerDid": "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoiaUpNUzVia1pWSWxuY2ZxX0xmX1N1eEoySnRRNUh2YXo3dFdQbkFqVVVkcyIsIngiOiJGWmR2d0M4YUdoUndxeldwdGVqME5aZ3R3WUFJMVN5RmcxbUtERVRPZnFFIn0",
"credentialConfigurationId": "UniversityDegree_jwt_vc_json",
"credentialData": {
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://www.w3.org/2018/credentials/examples/v1"
],
"id": "http://example.gov/credentials/3732",
"type": [
"VerifiableCredential",
"UniversityDegree"
],
"issuer": {
"id": "did:web:vc.transmute.world"
},
"issuanceDate": "2020-03-10T04:24:12.164Z",
"credentialSubject": {
"id": "did:example:ebfeb1f712ebc6f1c276e12ec21",
"degree": {
"type": "BachelorDegree",
"name": "Bachelor of Science and Arts"
}
}
},
"mapping": {
"id": "",
"issuer": {
"id": ""
},
"credentialSubject": {
"id": ""
},
"issuanceDate": "",
"expirationDate": ""
},
"authenticationMethod": "PRE_AUTHORIZED",
"standardVersion": "DRAFT13"
}
```
**Body Parameters**
- `issuerKey`: _JSON_ - A JWK or reference object to a key stored in an external KMS to sign the credential with.
Supported algorithms: ed25519, secp256k1, secp256r1, or
RSA.
**JWK Format:** `{"type": "jwk", "jwk": Here JSON Web Key Object}`. Must be provided as JWK object to "issuerKey".
**KMS Key**: Please refer to the [Key Management Section](https://docs.walt.id/community-stack/issuer/key-management/overview.md) and the
KMS you want to
use for more details on the structure
of the reference object.
- `issuerDid`: _String_ - The DID related to the provided key.
- `credentialConfigurationId`: _String_ - Reference to a specific credential configuration the issuer supports. As our
issuer currently supports W3C JWT & SD-JWT credentials and SD-JWT VCs (IETF). The structure of the
credentialConfigurationId is the following: "
credentialType_jwt_vc_json" for W3C JWT & SD-JWT credentials
and "credentialType_vc+sd-jwt" for SD-JWT VCs (IETF). E.g. "UniversityDegree_jwt_vc_json" or "
UniversityDegree_vc+sd-jwt".
You can also view
the credentialConfigurationIds by visiting the `/.well-known/openid-credential-issuer` endpoint of your issuer.
The metadata of our deployed issuer for testing can be
seen [here](https://issuer.demo.walt.id/swagger/index.html#/oidc/get__standardVersion___well_known_openid_credential_issuer).
- `credentialData`: _JSON_ - A credential data structure to sign (compliant with W3C, the World Wide Web Consortium standards). A list of predefined valid structures
can be
found [here](https://credentials.walt.id/).
- `mapping` (optional): _JSON_ - The mapping object that allows for **dynamic value insertion via data functions**,
executed at the time when the credentials is claimed. This feature enables personalized credentials based on real-time
data. Learn more about it and see a list of supported data
functions [here](https://docs.walt.id/community-stack/issuer/data-functions).
- `standardVersion`: (optional) _String_ - Defines which OIDC4VCI standard version is used. The supported standard
versions are: `DRAFT13` and `DRAFT11`. If no value is provided, it will default to `DRAFT13`.
- `authenticationMethod`: (optional) _String_ - Defines which OIDC4VC exchange flow is used (pre-auth or full-auth). If
no value is
provided, it will default to pre-auth flow indicated as `PRE_AUTHORIZED`. The parameter options are:
Expand To Learn More About The Options
- `PWD` - used for authorization code flow with username/password authentication with external auth server
- `ID_TOKEN` - used for authorization code flow with id_token authentication. When this method is used an authorization
request is sent to the wallet, which generates and issues the ID_TOKEN using its DID (
Decentralized Identifier). The issuer then verifies the ID_TOKEN to confirm the holder's identity and DID.
- `VP_TOKEN` - used for authorization code flow with vp_token(OIDC4VP). With this method, the receiver of the
credential must be authenticated by presenting the requested credential. The credential which must be presented can
be configured using the `vpRequestedValue` as explained below. The policies which are applied to the presented
credential
are 'signature,' 'expired,' and 'not-before.' Learn more about
policies [here](https://docs.walt.id/community-stack/verifier/credential-verification/policies/overview.md).
- `NONE` - used for authorization code flow with none authentication method
- `PRE_AUTHORIZED` - used for pre-authorizated code flow
**Additional parameters required for selected auth method:**
**For "ID_TOKEN" and "VP_TOKEN":**
- `useJar`: (Optional) _Boolean_ - used for using JAR OAuth specification in the id/vp_token requests. If omitted, the
default value is true.
**For VP_TOKEN:**
- `vpRequestedValue`: _String_ - Specifies the requested credential type value for the VP (Verifiable Presentation) token. E.g. "VerifiableId"
- `vpProfile`: (Optional) _String_ - Specifies the profile of the VP request. Available Profiles: DEFAULT: For W3C
OpenID4VP,
ISO_18013_7_MDOC: For MDOC OpenID4VP, EBSIV3: For EBSI V3 Compliant VP. If omitted, the default value is DEFAULT
---
**Example Response**
The issuer endpoint will respond with Credential Offer URL.
**Plain Response**
```
openid-credential-offer://issuer.portal.walt.id/?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fissuer.portal.walt.id%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22jwt_vc_json%22%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegree%22%5D%2C%22credential_definition%22%3A%7B%22%40context%22%3A%5B%22https%3A%2F%2Fwww.w3.org%2F2018%2Fcredentials%2Fv1%22%2C%22https%3A%2F%2Fwww.w3.org%2F2018%2Fcredentials%2Fexamples%2Fv1%22%5D%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegree%22%5D%7D%7D%5D%2C%22grants%22%3A%7B%22authorization_code%22%3A%7B%22issuer_state%22%3A%220431b78c-cd94-4f50-bfdf-e24d436c0cf6%22%7D%2C%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiIwNDMxYjc4Yy1jZDk0LTRmNTAtYmZkZi1lMjRkNDM2YzBjZjYiLCJpc3MiOiJodHRwczovL2lzc3Vlci5wb3J0YWwud2FsdC5pZCIsImF1ZCI6IlRPS0VOIn0.NorG7GtjmA-HXMJfUzU9vfnshcIgFY0oYQb8qJjDfORPoNxuurgySSOIDKFi7Z4XJmC-oJZnM0Nbb0NUd57cDA%22%2C%22user_pin_required%22%3Afalse%7D%7D%7D
```
**Decoded**
```
openid-credential-offer://issuer.portal.walt.id/?
credential_offer=
{
"credential_issuer": "https://issuer.demo.walt.id",
"credentials": [
{
"format": "jwt_vc_json",
"types": [
"VerifiableCredential",
"UniversityDegree"
],
"credential_definition": {
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://www.w3.org/2018/credentials/examples/v1"
],
"types": [
"VerifiableCredential",
"UniversityDegree"
]
}
}
],
"grants": {
"authorization_code": {
"issuer_state": "0431b78c-cd94-4f50-bfdf-e24d436c0cf6"
},
"urn:ietf:params:oauth:grant-type:pre-authorized_code": {
"pre-authorized_code": "eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiIwNDMxYjc4Yy1jZDk0LTRmNTAtYmZkZi1lMjRkNDM2YzBjZjYiLCJpc3MiOiJodHRwczovL2lzc3Vlci5wb3J0YWwud2FsdC5pZCIsImF1ZCI6IlRPS0VOIn0.NorG7GtjmA-HXMJfUzU9vfnshcIgFY0oYQb8qJjDfORPoNxuurgySSOIDKFi7Z4XJmC-oJZnM0Nbb0NUd57cDA",
"user_pin_required": false
}
}
}
```
The response will get from the issuer API will be an OIDCVI offer URL. This URL can then be passed to the wallet to
claim the credentials.
### Receive Credential in Wallet
**Option: CURL**
**Example Call
** | [Api Reference](https://wallet.demo.walt.id/swagger/index.html#/Credential%20exchange/post_wallet_api_wallet__wallet__exchange_useOfferRequest)
```bash
curl -X 'POST' \
'http://0.0.0.0:7001/wallet-api/wallet/{walletID}/exchange/useOfferRequest' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-H 'authorization: Bearer {loginToken}' \
-d '{credentialOfferFromIssuer}'
```
**Path Parameters**
- `walletID`: _String_ - the ID of our wallet, retrieved [previously](#retrieve-wallet-id). Provided as part of the URL path.
**Header Parameters**
- `authorization`: _String_ - Bearer token obtained from the [login step](#login-to-wallet-account). Format: `Bearer {loginToken}`.
**Body**
```json
credentialOfferFromIssuerAPI
```
**Note:**
When you are using the terminal the credentialOffer return from the issuer might have a `%` at the end of it. Make
sure to remove this before passing it to the wallet.
**Example Response**
```json
[
{
"wallet": "8abd7a5f-ae86-46e5-9693-1bb8a98f7c5d",
"id": "urn:uuid:d730bc77-8a9b-4f9b-8d56-50689aefe3d7",
"document": "eyJraWQiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0pqY25ZaU9pSkZaREkxTlRFNUlpd2lhMmxrSWpvaWFVcE5VelZpYTFwV1NXeHVZMlp4WDB4bVgxTjFlRW95U25SUk5VaDJZWG8zZEZkUWJrRnFWVlZrY3lJc0luZ2lPaUpHV21SMmQwTTRZVWRvVW5keGVsZHdkR1ZxTUU1YVozUjNXVUZKTVZONVJtY3hiVXRFUlZSUFpuRkZJbjAjaUpNUzVia1pWSWxuY2ZxX0xmX1N1eEoySnRRNUh2YXo3dFdQbkFqVVVkcyIsInR5cCI6IkpXVCIsImFsZyI6IkVkRFNBIn0.eyJpc3MiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0pqY25ZaU9pSkZaREkxTlRFNUlpd2lhMmxrSWpvaWFVcE5VelZpYTFwV1NXeHVZMlp4WDB4bVgxTjFlRW95U25SUk5VaDJZWG8zZEZkUWJrRnFWVlZrY3lJc0luZ2lPaUpHV21SMmQwTTRZVWRvVW5keGVsZHdkR1ZxTUU1YVozUjNXVUZKTVZONVJtY3hiVXRFUlZSUFpuRkZJbjAiLCJzdWIiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0pqY25ZaU9pSkZaREkxTlRFNUlpd2lhMmxrSWpvaVNtTXlWWHByY1RWRU5uaHJRbXAyWlRGVk5tRlJibU4yUzJkdFVrNTFTVU50Y1ZGTmIzVlFXVFJ5WXlJc0luZ2lPaUpFZEVoalgyTmZVSGgyY3pkb2NYRkRXRVZJTjIxWlRYSkZjWGgwY25jeVZWZGlRemhRYjA1WVRVMVJJbjAiLCJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL2V4YW1wbGVzL3YxIl0sImlkIjoidXJuOnV1aWQ6ZDczMGJjNzctOGE5Yi00ZjliLThkNTYtNTA2ODlhZWZlM2Q3IiwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsIlVuaXZlcnNpdHlEZWdyZWUiXSwiaXNzdWVyIjp7ImlkIjoiZGlkOmp3azpleUpyZEhraU9pSlBTMUFpTENKamNuWWlPaUpGWkRJMU5URTVJaXdpYTJsa0lqb2lhVXBOVXpWaWExcFdTV3h1WTJaeFgweG1YMU4xZUVveVNuUlJOVWgyWVhvM2RGZFFia0ZxVlZWa2N5SXNJbmdpT2lKR1dtUjJkME00WVVkb1VuZHhlbGR3ZEdWcU1FNWFaM1IzV1VGSk1WTjVSbWN4YlV0RVJWUlBabkZGSW4wIn0sImlzc3VhbmNlRGF0ZSI6IjIwMjUtMDQtMTZUMTI6MDg6MzEuOTMwOTQ0MDg1WiIsImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmp3azpleUpyZEhraU9pSlBTMUFpTENKamNuWWlPaUpGWkRJMU5URTVJaXdpYTJsa0lqb2lTbU15VlhwcmNUVkVObmhyUW1wMlpURlZObUZSYm1OMlMyZHRVazUxU1VOdGNWRk5iM1ZRV1RSeVl5SXNJbmdpT2lKRWRFaGpYMk5mVUhoMmN6ZG9jWEZEV0VWSU4yMVpUWEpGY1hoMGNuY3lWVmRpUXpoUWIwNVlUVTFSSW4wIiwiZGVncmVlIjp7InR5cGUiOiJCYWNoZWxvckRlZ3JlZSIsIm5hbWUiOiJCYWNoZWxvciBvZiBTY2llbmNlIGFuZCBBcnRzIn19LCJleHBpcmF0aW9uRGF0ZSI6IjIwMjYtMDQtMTZUMTI6MDg6MzEuOTMxMjU1NjY4WiJ9LCJqdGkiOiJ1cm46dXVpZDpkNzMwYmM3Ny04YTliLTRmOWItOGQ1Ni01MDY4OWFlZmUzZDciLCJleHAiOjE3NzYzNDEzMTEsImlhdCI6MTc0NDgwNTMxMSwibmJmIjoxNzQ0ODA1MzExfQ.tjhXghX6WtMYm0R31cULd0aKVrqRRFkINI-KEjYsDeyoALxl833dEdESosLKUm197Yh8WK5QmX0jMc-gyvnnCw",
"disclosures": "",
"addedOn": "2025-04-16T12:08:32.036587543Z",
"pending": false,
"format": "jwt_vc_json",
"parsedDocument": {
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://www.w3.org/2018/credentials/examples/v1"
],
"id": "urn:uuid:d730bc77-8a9b-4f9b-8d56-50689aefe3d7",
"type": [
"VerifiableCredential",
"UniversityDegree"
],
"issuer": {
"id": "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoiaUpNUzVia1pWSWxuY2ZxX0xmX1N1eEoySnRRNUh2YXo3dFdQbkFqVVVkcyIsIngiOiJGWmR2d0M4YUdoUndxeldwdGVqME5aZ3R3WUFJMVN5RmcxbUtERVRPZnFFIn0"
},
"issuanceDate": "2025-04-16T12:08:31.930944085Z",
"credentialSubject": {
"id": "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoiSmMyVXprcTVENnhrQmp2ZTFVNmFRbmN2S2dtUk51SUNtcVFNb3VQWTRyYyIsIngiOiJEdEhjX2NfUHh2czdocXFDWEVIN21ZTXJFcXh0cncyVVdiQzhQb05YTU1RIn0",
"degree": {
"type": "BachelorDegree",
"name": "Bachelor of Science and Arts"
}
},
"expirationDate": "2026-04-16T12:08:31.931255668Z"
}
}
]
```
## Request & Verify Credential
In the next section, we will be using the walt.id verifier API to generate an OID4VP authorization request. This request
we will then pass to the wallet to present the credential we received and stored in the previous step.
### Create Authorization Request
Below, we will outline the specific credentials and the format we wish to request from a wallet. In our example, we will
be requesting a `UniversityDegree` in the W3C Verifiable Credential (VC) format (`jwt_vc_json`). The Verifier API will
take the incoming request and convert it into a Presentation Definition which becomes part of the returned
Authorization request. The Authorization request with the presentation definition allows the wallet to understand which
credentials it needs to present to the verifier.
**Option: CURL**
```bash
curl -X 'POST' \
'http://0.0.0.0:7003/openid4vc/verify' \
-H 'accept: */*' \
-H 'authorizeBaseUrl: openid4vp://authorize' \
-H 'responseMode: direct_post' \
-H 'Content-Type: application/json' \
-d '{
"request_credentials": [
{ "type": "UniversityDegree", "format": "jwt_vc_json" }
]
}'
```
**Header Parameters**
- **authorizeBaseUrl** - is used to modify the start of the OID4VC request URL. If you are using the
cross-device
flow, where you will display the URL as a QR code, you can leave the value as `openid4vp://authorize` or if you don't
know the wallet the user will be using to claim the credential. If you are using the same device flow, where you
already
know the user's wallet and want the user to be able to go directly to it, you can use the wallet URL path that is able
to
receive an OIDC (OpenID Connect) request as a query parameter. Our wallet for example can receive OID4VC requests here
`https://wallet.demo.walt.id/wallet-api/wallet/{wallet}/exchange/useOfferRequest`.
- **responseMode** - should be `direct_post` as the other options are not yet supported.
- **successRedirectUri** (optional) - is used to redirect the user if verification is successful. You can use the `$id`
placeholder
to get access to the id of verification session in your application in order to retrieve the verification results.
E.g. `/success?id=$id` will be replaced with `/success?id=1234567890`
- **errorRedirectUri** (optional) - is used to redirect the user if verification is unsuccessful. You can use the `$id`
placeholder
to get access to the id of verification session in your application in order to retrieve the verification results.
E.g. `/error?id=$id` will be replaced with `/error?id=1234567890`
- **statusCallbackUri** (optional) - URL that should be called when the presentation request has been fulfilled by a
wallet. The request
sent will be a `POST` including the whole presentation
result. [See Verification Status Policies Response](#verification-status-policies-response)
- **statusCallbackApiKey** (optional) - If the endpoint you provide via `statusCallbackUri` is protected, you can use
the `statusCallbackApiKey` to authenticate.
The provided key will be appended as Authorization Header to the request.
- **stateId** (optional) - overwrite the unique `state` value which gets created for each verification request with your
own.
- **openId4VPProfile** (optional) - Define profile for VP (Verifiable Presentation) request. The default is W3C
OpenID4VP, which can optionally provided as `DEFAULT`.
Apart from that, you can choose from the following options:
- `EBSIV3`: For EBSI V3 compliant VP. Learn more [here](https://docs.walt.id/community-stack/verifier/ecosystems/ebsi/overview.md).
- `ISO_18013_7_MDOC`: For mdoc Openid4VP.
**Body**
```json
{
"request_credentials": [
{
"type": "UniversityDegree",
"format": "jwt_vc_json"
}
]
}
```
**Body Parameters**
- **request_credentials** - An array of objects detailing the credentials to be requested from the user. Each object
varies based on the type of credential being requested.
Expand To Learn More
Below are the possible credential types and their respective object structures:
1. **W3C JWT Credential**
- **type** Specifies the type of credential (e.g., `VerifiableID`, `VerifiableDiploma`). This maps to the `type`
attribute in W3C credentials.
- **format**: Describes the format of the credential. For W3C JWT credentials, this would
be `jwt_vc_json`.
```json
{ "type": "ProofOfResidence", "format": "jwt_vc_json" }
```
2. **SD-JWT VC Credential (IETF Standard)**
- **vct**: Specifies the type of credential (e.g., https://issuer.com/identity_credential). This maps to
the `vct`
attribute in the SD-JWT VC credential.
- **format**: Describes the format of the credential. For SD-JWT VC credentials, this would be
vc+sd-jwt.: Describes the format of the credential. For SD-JWT VC credentials, this would be `vc+sd-jwt`.
```json
{ "vct": "test.com/identity_credential", "format": "vc+sd-jwt" }
```
3. **mDL Credential (ISO/IEC 18013-5)**
- **doc_type**: Specifies the type of credential (e.g., org.iso.18013.5.1.mDL). This maps to the doc_type
attribute of
the mdoc document.
- **format**: Describes the format of the credential. For mDL credentials, this would be `mso_mdoc`.
```json
{ "doc_type": "org.iso.18013.5.1.mDL", "format": "mso_mdoc" }
```
**Optional Parameters**
Next to describing the type and format of the credential, the objects also take an optional `policies` and `id`
attribute
- **policies**: An array of policies to apply to the specified credential. A list of all policies can be
found [here](https://docs.walt.id/community-stack/verifier/credential-verification/policies/overview.md).
- **id**: Used to set a specific id for the
generated [Presentation Definition](https://identity.foundation/presentation-exchange/spec/v1.0.0/#presentation-definition).
If not set,
the verifier API auto-assigns a generated one.
**Full Examples**
```json
{
"type": "ProofOfResidence",
"format": "jwt_vc_json",
"policies": [
"schema",
{
"policy": "webhook",
"args": "https://example.org/abc/xyz"
}
],
"id": "test123"
}
```
**Example Response**
**Raw Response**
```text
openid4vp://authorize?response_type=vp_token&client_id=http%3A%2F%2Fhost.docker.internal%3A7003%2Fopenid4vc%2Fverify&response_mode=direct_post&state=5smqTtxA1L0F&presentation_definition_uri=http%3A%2F%2Fhost.docker.internal%3A7003%2Fopenid4vc%2Fpd%2F5smqTtxA1L0F&client_id_scheme=redirect_uri&client_metadata=%7B%22authorization_encrypted_response_alg%22%3A%22ECDH-ES%22%2C%22authorization_encrypted_response_enc%22%3A%22A256GCM%22%7D&nonce=2afa2fdc-b8ab-4d4c-a4b6-bd299949fb4b&response_uri=http%3A%2F%2Fhost.docker.internal%3A7003%2Fopenid4vc%2Fverify%2F5smqTtxA1L0F
```
**Decoded Response**
```text
openid4vp://authorize?
response_type=vp_token
&client_id=http://host.docker.internal:7003/openid4vc/verify
&response_mode=direct_post
&state=5smqTtxA1L0F
&presentation_definition_uri=http://host.docker.internal:7003/openid4vc/pd/5smqTtxA1L0F
&client_id_scheme=redirect_uri
&client_metadata={
"authorization_encrypted_response_alg":"ECDH-ES",
"authorization_encrypted_response_enc":"A256GCM"
}
&nonce=2afa2fdc-b8ab-4d4c-a4b6-bd299949fb4b
&response_uri=http://host.docker.internal:7003/openid4vc/verify/5smqTtxA1L0F
```
**Presentation Definition URI result**
Below we see the [presentation definition](https://identity.foundation/presentation-exchange/spec/v2.0.0/) the verifier
API has generated based on our request. This helps the wallet find matching credentials to present.
```json
{
"id": "EjRyJNiWqKcc",
"input_descriptors": [
{
"id": "UniversityDegree",
"format": {
"jwt_vc_json": {
"alg": [
"EdDSA"
]
}
},
"constraints": {
"fields": [
{
"path": [
"$.vc.type"
],
"filter": {
"type": "string",
"pattern": "UniversityDegree"
}
}
]
}
}
]
}
```
## Present Credential
### 1. Extract Presentation Definition
First, we will need to decode the Authorization Request we got from the verifier.
**Example Authorization Request**
```text
openid4vp://authorize?response_type=vp_token&client_id=http%3A%2F%2Fhost.docker.internal%3A7003%2Fopenid4vc%2Fverify&response_mode=direct_post&state=5smqTtxA1L0F&presentation_definition_uri=http%3A%2F%2Fhost.docker.internal%3A7003%2Fopenid4vc%2Fpd%2F5smqTtxA1L0F&client_id_scheme=redirect_uri&client_metadata=%7B%22authorization_encrypted_response_alg%22%3A%22ECDH-ES%22%2C%22authorization_encrypted_response_enc%22%3A%22A256GCM%22%7D&nonce=2afa2fdc-b8ab-4d4c-a4b6-bd299949fb4b&response_uri=http%3A%2F%2Fhost.docker.internal%3A7003%2Fopenid4vc%2Fverify%2F5smqTtxA1L0F
```
For the decoding we can use an online tool like [this](https://www.url-encode-decode.com/){:target="_blank"}.
**Example of Decoded Authorization Request**
```text
openid4vp://authorize?
response_type=vp_token
&client_id=http://host.docker.internal:7003/openid4vc/verify
&response_mode=direct_post
&state=5smqTtxA1L0F
&presentation_definition_uri=http://host.docker.internal:7003/openid4vc/pd/5smqTtxA1L0F
&client_id_scheme=redirect_uri
&client_metadata={
"authorization_encrypted_response_alg":"ECDH-ES",
"authorization_encrypted_response_enc":"A256GCM"
}
&nonce=2afa2fdc-b8ab-4d4c-a4b6-bd299949fb4b
&response_uri=http://host.docker.internal:7003/openid4vc/verify/5smqTtxA1L0F
```
Now we need to visit the `presentation_definition_uri` provided and extract the presentation definition.
**Example Presentation Definition**
```json
{
"id": "EjRyJNiWqKcc",
"input_descriptors": [
{
"id": "UniversityDegree",
"format": {
"jwt_vc_json": {
"alg": [
"EdDSA"
]
}
},
"constraints": {
"fields": [
{
"path": [
"$.vc.type"
],
"filter": {
"type": "string",
"pattern": "UniversityDegree"
}
}
]
}
}
]
}
```
### 2. Find Credential(s) Matching Presentation Definition
Use the `matchCredentialsForPresentationDefinition` endpoint in the wallet API to find all user credentials matching the
presentation request constraints.
**Option: CURL**
**Endpoint:**
`/wallet-api/wallet/{wallet}/exchange/matchCredentialsForPresentationDefinition`| [API Reference](https://wallet.demo.walt.id/swagger/index.html#/Credential%20exchange/post_wallet_api_wallet__wallet__exchange_matchCredentialsForPresentationDefinition)
**Example Request**
```bash
curl -X 'POST' \
'http://0.0.0.0:7001/wallet-api/wallet/{wallet_id}/exchange/matchCredentialsForPresentationDefinition' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-H 'authorization: Bearer {loginToken}' \
-d '{presentationDefinitionExtractedFromAuthorizationRequest}'
```
**Path Parameters**
- **wallet_id** - Use the saved wallet ID from earlier or extract it via the `/wallet-api/wallet/accounts/wallets`
endpoint as explained [here](#retrieve-wallet-id). Provided as part of the URL path.
**Header Parameters**
- `authorization`: _String_ - Bearer token obtained from the [login step](#login-to-wallet-account). Format: `Bearer {loginToken}`.
**Body**
The extracted presentation definition as JSON, as explained above.
```json
{
"id": "EjRyJNiWqKcc",
"input_descriptors": [
{
"id": "UniversityDegree",
"format": {
"jwt_vc_json": {
"alg": [
"EdDSA"
]
}
},
"constraints": {
"fields": [
{
"path": [
"$.vc.type"
],
"filter": {
"type": "string",
"pattern": "UniversityDegree"
}
}
]
}
}
]
}
```
---
**Example Response**
If credentials matching the presentation definition are found, a list of these credentials will be returned. If no match
is found, an empty list will be returned. Take a note of the credential id(s) returned as we will be needing them in
a later step.
```json
[
{
"wallet": "5f2eb7d7-7d04-461c-b93d-28d95efbf15b",
// ⬇️ Note for later ⬇️
"id": "urn:uuid:fb09ba3d-b73d-49ad-9d88-3d018878ea83",
// ⬆️ Note for later ⬆️
"document": "eyJraWQiOiJkaWQ6andrOmV5SnJkSGtpT2lKRlF5SXNJbU55ZGlJNklsQXRNalUySWl3aWEybGtJam9pTTFsT1pEbEdibmc1U214NVVGWlpkMmRYUmtVek4wVXpSM2RKTUdWSGJFTkxPSGRHYkZkNFIyWndUU0lzSW5naU9pSkdiM1paTWpGTVFVRlBWR3huTFcwdFRtVkxWMmhhUlV3MVlVWnlibEl3ZFdOS2FrUTFWRXR3UjNWbklpd2llU0k2SWtOeVJrcG1SMVJrVURJNVNrcGpZM0JSV0hWNVRVOHpiMmgwZW5KVWNWQjZRbEJDU1ZSWmFqQnZaMEVpZlEiLCJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJkaWQ6andrOmV5SnJkSGtpT2lKRlF5SXNJbU55ZGlJNklsQXRNalUySWl3aWEybGtJam9pTTFsT1pEbEdibmc1U214NVVGWlpkMmRYUmtVek4wVXpSM2RKTUdWSGJFTkxPSGRHYkZkNFIyWndUU0lzSW5naU9pSkdiM1paTWpGTVFVRlBWR3huTFcwdFRtVkxWMmhhUlV3MVlVWnlibEl3ZFdOS2FrUTFWRXR3UjNWbklpd2llU0k2SWtOeVJrcG1SMVJrVURJNVNrcGpZM0JSV0hWNVRVOHpiMmgwZW5KVWNWQjZRbEJDU1ZSWmFqQnZaMEVpZlEiLCJzdWIiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0pqY25ZaU9pSkZaREkxTlRFNUlpd2lhMmxrSWpvaVEyVTVNbGczUVhVMVRuQlRWV0ZoWlU5VFl6TkpSMjlDTFVacFNUTmtaMjFYT1Y5c2NGTldja3hrYXlJc0luZ2lPaUpIY1VabWRqbFNjemh1TlRrMk4wSXlVR3g0TW1wbFduRnlOWFZxVkRoM2RYWk5XbEpIYlZoeGVFSkZJbjAiLCJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiVmVyaWZpYWJsZUF0dGVzdGF0aW9uIiwiVmVyaWZpYWJsZUlkIl0sImNyZWRlbnRpYWxTY2hlbWEiOnsiaWQiOiJodHRwczovL2FwaS5wcmVwcm9kLmVic2kuZXUvdHJ1c3RlZC1zY2hlbWFzLXJlZ2lzdHJ5L3YxL3NjaGVtYXMvMHhiNzdmODUxNmE5NjU2MzFiNGYxOTdhZDU0YzY1YTllMmY5OTM2ZWJmYjc2YmFlNDkwNmQzMzc0NGRiY2M2MGJhIiwidHlwZSI6IkZ1bGxKc29uU2NoZW1hVmFsaWRhdG9yMjAyMSJ9LCJjcmVkZW50aWFsU3ViamVjdCI6eyJjdXJyZW50QWRkcmVzcyI6WyIxIEJvdWxldmFyZCBkZSBsYSBMaWJlcnTDqSwgNTk4MDAgTGlsbGUiXSwiZGF0ZU9mQmlydGgiOiIxOTkzLTA0LTA4IiwiZmFtaWx5TmFtZSI6IkRPRSIsImZpcnN0TmFtZSI6IkphbmUiLCJnZW5kZXIiOiJGRU1BTEUiLCJpZCI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWEybGtJam9pUTJVNU1sZzNRWFUxVG5CVFZXRmhaVTlUWXpOSlIyOUNMVVpwU1ROa1oyMVhPVjlzY0ZOV2NreGtheUlzSW5naU9pSkhjVVptZGpsU2N6aHVOVGsyTjBJeVVHeDRNbXBsV25GeU5YVnFWRGgzZFhaTldsSkhiVmh4ZUVKRkluMCIsIm5hbWVBbmRGYW1pbHlOYW1lQXRCaXJ0aCI6IkphbmUgRE9FIiwicGVyc29uYWxJZGVudGlmaWVyIjoiMDkwNDAwODA4NEgiLCJwbGFjZU9mQmlydGgiOiJMSUxMRSwgRlJBTkNFIn0sImV2aWRlbmNlIjpbeyJkb2N1bWVudFByZXNlbmNlIjpbIlBoeXNpY2FsIl0sImV2aWRlbmNlRG9jdW1lbnQiOlsiUGFzc3BvcnQiXSwic3ViamVjdFByZXNlbmNlIjoiUGh5c2ljYWwiLCJ0eXBlIjpbIkRvY3VtZW50VmVyaWZpY2F0aW9uIl0sInZlcmlmaWVyIjoiZGlkOmVic2k6MkE5Qlo5U1VlNkJhdGFjU3B2czFWNUNkakh2THBRN2JFc2kySmI2TGRIS25ReGFOIn1dLCJpZCI6InVybjp1dWlkOmZiMDliYTNkLWI3M2QtNDlhZC05ZDg4LTNkMDE4ODc4ZWE4MyIsImlzc3VlZCI6IjIwMjEtMDgtMzFUMDA6MDA6MDBaIiwiaXNzdWVyIjoiZGlkOmp3azpleUpyZEhraU9pSkZReUlzSW1OeWRpSTZJbEF0TWpVMklpd2lhMmxrSWpvaU0xbE9aRGxHYm5nNVNteDVVRlpaZDJkWFJrVXpOMFV6UjNkSk1HVkhiRU5MT0hkR2JGZDRSMlp3VFNJc0luZ2lPaUpHYjNaWk1qRk1RVUZQVkd4bkxXMHRUbVZMVjJoYVJVdzFZVVp5YmxJd2RXTktha1ExVkV0d1IzVm5JaXdpZVNJNklrTnlSa3BtUjFSa1VESTVTa3BqWTNCUldIVjVUVTh6YjJoMGVuSlVjVkI2UWxCQ1NWUlphakJ2WjBFaWZRIiwidmFsaWRGcm9tIjoiMjAyMS0wOC0zMVQwMDowMDowMFoiLCJpc3N1YW5jZURhdGUiOiIyMDI0LTA4LTIxVDA5OjExOjMzLjI0NjM4OTA0MVoifSwianRpIjoidXJuOnV1aWQ6ZmIwOWJhM2QtYjczZC00OWFkLTlkODgtM2QwMTg4NzhlYTgzIiwiaWF0IjoxNzI0MjMxNDkzLCJuYmYiOjE3MjQyMzE0OTN9.OsPTKQC6tvD6TtxeoCd8FtaIThYrYeYRjCHzDXkmgTUjRw78faT9R2bljT03ncrb0YstK0EmjM9D5lwqMZ9ZIA",
"addedOn": "2024-08-21T09:11:33.284Z",
"format": "jwt_vc"
}
]
```
### 3. Get User Consent (not needed for this tutorial)
Normally, you should ask for the owner's consent before presenting the credentials matched in the previous step. Only if the user agrees should you
proceed with sharing them. As we are the user, we don't need this step.
### 4. Share Credential(s) With Verifier
Now that we have identified the credentials to share and received confirmation from the wallet owner, we are ready to
reply to the verifier's presentation request.
However, before we can do so we need to resolve the original presentation
request using the following endpoint `/exchange/resolvePresentationRequest`
#### 4.1. Resolve Presentation Request
**Option: CURL**
**Endpoint:**
`/wallet-api/wallet/{wallet}/exchange/resolvePresentationRequest`| [API Reference](https://wallet.demo.walt.id/swagger/index.html#/Credential%20exchange/post_wallet_api_wallet__wallet__exchange_resolvePresentationRequest)
**Example Request**
```bash
curl -X 'POST' \
'http://0.0.0.0:7001/wallet-api/wallet/{wallet_id}/exchange/resolvePresentationRequest' \
-H 'accept: text/plain' \
-H 'Content-Type: text/plain' \
-H 'authorization: Bearer {loginToken}' \
-d '{authorizationRequestFromVerifier}'
```
**Path Parameters**
- **wallet_id** - Extract needed wallet id parameter via the `/wallet-api/wallet/accounts/wallets` endpoint as explained [here](#retrieve-wallet-id). Provided as part of the URL path.
**Header Parameters**
- `authorization`: _String_ - Bearer token obtained from the [login step](#login-to-wallet-account). Format: `Bearer {loginToken}`.
**Body**
The original presentation request received from the verifier.
```
openid4vp://authorize?
response_type=vp_token&
client_id=https%3A%2F%2Fverifier.demo.walt.id%2Fopenid4vc%2Fverify&
response_mode=direct_post&
state=V3pp6GFoSro6&
presentation_definition_uri=https%3A%2F%2Fverifier.demo.walt.id%2Fopenid4vc%2Fpd%2FV3pp6GFoSro6&
client_id_scheme=redirect_uri&
client_metadata=%7B%22authorization_encrypted_response_alg%22%3A%22ECDH-ES%22%2C%22authorization_encrypted_response_enc%22%3A%22A256GCM%22%7D&
nonce=0c4e8d3c-1017-4a38-b6d2-468e4a2c13f8&
response_uri=https%3A%2F%2Fverifier.demo.walt.id%2Fopenid4vc%2Fverify%2FV3pp6GFoSro6
```
**Body Parameters**
The original presentation request received from the verifier.
---
**Example Response**
```
openid4vp://authorize?response_type=vp_token&client_id=https%3A%2F%2Fverifier.demo.walt.id%2Fopenid4vc%2Fverify&response_mode=direct_post&state=V3pp6GFoSro6&presentation_definition=%7B%22id%22%3A%22GgLtLahfRjuK%22%2C%22input_descriptors%22%3A%5B%7B%22id%22%3A%22VerifiableId%22%2C%22format%22%3A%7B%22jwt_vc_json%22%3A%7B%22alg%22%3A%5B%22EdDSA%22%5D%7D%7D%2C%22constraints%22%3A%7B%22fields%22%3A%5B%7B%22path%22%3A%5B%22%24.type%22%5D%2C%22filter%22%3A%7B%22type%22%3A%22string%22%2C%22pattern%22%3A%22VerifiableId%22%7D%7D%5D%7D%7D%5D%7D&presentation_definition_uri=https%3A%2F%2Fverifier.demo.walt.id%2Fopenid4vc%2Fpd%2FV3pp6GFoSro6&client_id_scheme=redirect_uri&client_metadata=%7B%22authorization_encrypted_response_alg%22%3A%22ECDH-ES%22%2C%22authorization_encrypted_response_enc%22%3A%22A256GCM%22%7D&nonce=0c4e8d3c-1017-4a38-b6d2-468e4a2c13f8&response_uri=https%3A%2F%2Fverifier.demo.walt.id%2Fopenid4vc%2Fverify%2FV3pp6GFoSro6
```
#### 4.2. Fulfill Presentation Request
**Option: CURL**
**Endpoint:**
`/wallet-api/wallet/{wallet}/exchange/usePresentationRequest`| [API Reference](https://wallet.demo.walt.id/swagger/index.html#/Credential%20exchange/post_wallet_api_wallet__wallet__exchange_usePresentationRequest)
**Example Request**
```bash
curl -X 'POST' \
'http://0.0.0.0:7001/wallet-api/wallet/{wallet_id}/exchange/usePresentationRequest' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-H 'authorization: Bearer {loginToken}' \
-d '{
"presentationRequest": "{resolvedPresentationRequestFromLastStep}",
"selectedCredentials": [
"{credentialIDMatchFromStepBeforeLastStep}"
]
}'
```
**Path Parameters**
- **wallet_id** - Extract needed wallet id parameter via the `/wallet-api/wallet/accounts/wallets` endpoint as explained [here](#retrieve-wallet-id). Provided as part of the URL path.
**Header Parameters**
- `authorization`: _String_ - Bearer token obtained from the [login step](#login-to-wallet-account). Format: `Bearer {loginToken}`.
**Body**
```json
{
"presentationRequest": "openid4vp://authorize?response_type=vp_token&client_id=https%3A%2F%2Fverifier.demo.walt.id%2Fopenid4vc%2Fverify&response_mode=direct_post&state=pXZz119vn5SG&presentation_definition=%7B%22id%22%3A%222k6OTMoo2wbU%22%2C%22input_descriptors%22%3A%5B%7B%22id%22%3A%22VerifiableId%22%2C%22format%22%3A%7B%22jwt_vc_json%22%3A%7B%22alg%22%3A%5B%22EdDSA%22%5D%7D%7D%2C%22constraints%22%3A%7B%22fields%22%3A%5B%7B%22path%22%3A%5B%22%24.type%22%5D%2C%22filter%22%3A%7B%22type%22%3A%22string%22%2C%22pattern%22%3A%22VerifiableId%22%7D%7D%5D%7D%7D%5D%7D&presentation_definition_uri=https%3A%2F%2Fverifier.demo.walt.id%2Fopenid4vc%2Fpd%2FpXZz119vn5SG&client_id_scheme=redirect_uri&client_metadata=%7B%22authorization_encrypted_response_alg%22%3A%22ECDH-ES%22%2C%22authorization_encrypted_response_enc%22%3A%22A256GCM%22%7D&nonce=d319f72a-cadf-4b15-827f-8ed0e9f755c0&response_uri=https%3A%2F%2Fverifier.demo.walt.id%2Fopenid4vc%2Fverify%2FpXZz119vn5SG",
"selectedCredentials": [
"urn:uuid:fb09ba3d-b73d-49ad-9d88-3d018878ea83"
]
}
```
**Body Parameters**
- `presentationRequest`: _String_ - response from previous call
to [resolvePresentationRequest](https://docs.walt.id/community-stack/wallet/credential-exchange/guides/present-vc-oid4vp#_41-resolve-presentation-request)
- `selectedCredentials`: _Array_ - list of credential id's to share which we received from the call
to [find matching credentials](https://docs.walt.id/community-stack/wallet/credential-exchange/guides/present-vc-oid4vp#_2-find-credentials-matching-presentation-definition)
- `disclosures`: (optional) _Object_ - This object specifies which selectively disclosable fields of a credential should
be shared with the verifier. It is applicable only if the credential to be shared contains selective disclosure
attributes. The object uses the credential ID as the key(s) and an array of the disclosures of the fields to be
disclosed by credential as values.
**Example:**
```json
"disclosures": {
"urn:uuid:fb09ba3d-b73d-49ad-9d88-3d018878ea83": [
"WyJiZ1I5OTdnRUVRNEU5bFpXNEhwVjRRPT0iLCJuYW1lIiwiSkZGIHggdmMtZWR1IFBsdWdGZXN0IDMgSW50ZXJvcGVyYWJpbGl0eSJd"
],
"urn:uuid:fb0234-b7234d-55ad-25d88-3d018878ea83": [
"WyJiZ1I5OTdnRUVRNEU5bFpXNEhwVjRRPT0iLCJuYW1lIiwiSkZGIHggdmMtZWR1IFBsdWdGZXN0IDMgSW50ZXJvcGVyYWJpbGl0eSJd"
]
}
```
**Explanation:**
- **Key**: The unique identifier (ID) of the credential to be shared.
- **Value**: An array containing the disclosures of credential fields that should be disclosed.
This structure allows you to specify which parts of a credential are shared on a per-credential basis, enhancing
privacy and control over the information disclosed.
---
**Example Response**
If the verifier provided a redirect URI we will receive it as a response.
```json
{
"redirectUri": null
}
```
## Verify Credential Presentation
Now we return to the Verifier to confirm whether the information provided by the wallet is valid and has successfully
passed all necessary checks.
**Option: CURL**
**Endpoint:**
`/openid4vc/session/{id}`| [API Reference](https://verifier.demo.walt.id/swagger/index.html#/Credential%20Verification/get_openid4vc_session__id_)
First, we need to return to the original Authorization Request that [we created](#create-authorization-request) and
retrieve the `state` value from it.
This state value serves as a unique identifier for this specific verification request.
**Example**
```
openid4vp://authorize?...state=a07bdb17-7d87-4965-9296-1adefcaaddd9...
```
Making the call to receive the verification result
**Example Call**
```bash
curl -X 'GET' \
'http://0.0.0.0:7003/openid4vc/session/{state}' \
-H 'accept: */*'
```
**Path Parameters**
- `state` _String_ - state value from [authorization request created](#create-authorization-request) via
the `openid4vc/verify` endpoint.
**Example Response**
```json
{
"id": "Ag9gyQkYknmI",
"presentationDefinition": {
"id": "8cu3ixcb4cm",
"input_descriptors": [
{
"id": "UniversityDegree",
"format": {
"jwt_vc_json": {
"alg": [
"EdDSA"
]
}
},
"constraints": {
"fields": [
{
"path": [
"$.vc.type"
],
"filter": {
"type": "string",
"pattern": "UniversityDegree"
}
}
]
}
}
],
"customParameters": {}
},
"tokenResponse": {
"vp_token": "eyJraWQiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0pqY25ZaU9pSkZaREkxTlRFNUlpd2lhMmxrSWpvaWVWUk9XRXBFVUZKRU0wUnFVSGRYYXpBMFNHeEhVbGhzVkVod09WTk5OVE4xY0hnelRGcEpMV1JrVlNJc0luZ2lPaUpMWVUwelYxazRObWR4ZHpGdFdsUnRORk00VmtNMGJHeG5RV1ZKVjI1Q2FUZHliMEZ2WVMwNWNVWnZJbjAjMCIsInR5cCI6IkpXVCIsImFsZyI6IkVkRFNBIn0.eyJzdWIiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0pqY25ZaU9pSkZaREkxTlRFNUlpd2lhMmxrSWpvaWVWUk9XRXBFVUZKRU0wUnFVSGRYYXpBMFNHeEhVbGhzVkVod09WTk5OVE4xY0hnelRGcEpMV1JrVlNJc0luZ2lPaUpMWVUwelYxazRObWR4ZHpGdFdsUnRORk00VmtNMGJHeG5RV1ZKVjI1Q2FUZHliMEZ2WVMwNWNVWnZJbjAiLCJuYmYiOjE3NDQ4NzczNjAsImlhdCI6MTc0NDg3NzQyMCwianRpIjoiOGN1M2l4Y2I0Y20iLCJpc3MiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0pqY25ZaU9pSkZaREkxTlRFNUlpd2lhMmxrSWpvaWVWUk9XRXBFVUZKRU0wUnFVSGRYYXpBMFNHeEhVbGhzVkVod09WTk5OVE4xY0hnelRGcEpMV1JrVlNJc0luZ2lPaUpMWVUwelYxazRObWR4ZHpGdFdsUnRORk00VmtNMGJHeG5RV1ZKVjI1Q2FUZHliMEZ2WVMwNWNVWnZJbjAiLCJub25jZSI6ImIxNmZkNDY2LTM4NDktNDc2Mi1hZGE4LTJjZmU3MzdkYWJhNSIsImF1ZCI6Imh0dHA6Ly9ob3N0LmRvY2tlci5pbnRlcm5hbDo3MDAzL29wZW5pZDR2Yy92ZXJpZnkiLCJ2cCI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVQcmVzZW50YXRpb24iXSwiaWQiOiI4Y3UzaXhjYjRjbSIsImhvbGRlciI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWEybGtJam9pZVZST1dFcEVVRkpFTTBScVVIZFhhekEwU0d4SFVsaHNWRWh3T1ZOTk5UTjFjSGd6VEZwSkxXUmtWU0lzSW5naU9pSkxZVTB6VjFrNE5tZHhkekZ0V2xSdE5GTTRWa00wYkd4blFXVkpWMjVDYVRkeWIwRnZZUzA1Y1VadkluMCIsInZlcmlmaWFibGVDcmVkZW50aWFsIjpbImV5SnJhV1FpT2lKa2FXUTZhbmRyT21WNVNuSmtTR3RwVDJsS1VGTXhRV2xNUTBwcVkyNVphVTlwU2taYVJFa3hUbFJGTlVscGQybGhNbXhyU1dwdmFXRlZjRTVWZWxacFlURndWMU5YZUhWWk1scDRXREI0YlZneFRqRmxSVzk1VTI1U1VrNVZhREpaV0c4elpFWmtVV0pyUm5GV1ZsWnJZM2xKYzBsdVoybFBhVXBIVjIxU01tUXdUVFJaVldSdlZXNWtlR1ZzWkhka1IxWnhUVVUxWVZvelVqTlhWVVpLVFZaT05WSnRZM2hpVlhSRlVsWlNVRnB1UmtaSmJqQWphVXBOVXpWaWExcFdTV3h1WTJaeFgweG1YMU4xZUVveVNuUlJOVWgyWVhvM2RGZFFia0ZxVlZWa2N5SXNJblI1Y0NJNklrcFhWQ0lzSW1Gc1p5STZJa1ZrUkZOQkluMC5leUpwYzNNaU9pSmthV1E2YW5kck9tVjVTbkprU0d0cFQybEtVRk14UVdsTVEwcHFZMjVaYVU5cFNrWmFSRWt4VGxSRk5VbHBkMmxoTW14clNXcHZhV0ZWY0U1VmVsWnBZVEZ3VjFOWGVIVlpNbHA0V0RCNGJWZ3hUakZsUlc5NVUyNVNVazVWYURKWldHOHpaRVprVVdKclJuRldWbFpyWTNsSmMwbHVaMmxQYVVwSFYyMVNNbVF3VFRSWlZXUnZWVzVrZUdWc1pIZGtSMVp4VFVVMVlWb3pVak5YVlVaS1RWWk9OVkp0WTNoaVZYUkZVbFpTVUZwdVJrWkpiakFpTENKemRXSWlPaUprYVdRNmFuZHJPbVY1U25Ka1NHdHBUMmxLVUZNeFFXbE1RMHBxWTI1WmFVOXBTa1phUkVreFRsUkZOVWxwZDJsaE1teHJTV3B2YVdWV1VrOVhSWEJGVlVaS1JVMHdVbkZWU0dSWVlYcEJNRk5IZUVoVmJHaHpWa1ZvZDA5V1RrNU9WRTR4WTBobmVsUkdjRXBNVjFKclZsTkpjMGx1WjJsUGFVcE1XVlV3ZWxZeGF6Uk9iV1I0WkhwR2RGZHNVblJPUmswMFZtdE5NR0pIZUc1UlYxWktWakkxUTJGVVpIbGlNRVoyV1ZNd05XTlZXblpKYmpBaUxDSjJZeUk2ZXlKQVkyOXVkR1Y0ZENJNld5Sm9kSFJ3Y3pvdkwzZDNkeTUzTXk1dmNtY3ZNakF4T0M5amNtVmtaVzUwYVdGc2N5OTJNU0lzSW1oMGRIQnpPaTh2ZDNkM0xuY3pMbTl5Wnk4eU1ERTRMMk55WldSbGJuUnBZV3h6TDJWNFlXMXdiR1Z6TDNZeElsMHNJbWxrSWpvaWRYSnVPblYxYVdRNk9EQmhPREUzTnpFdE56azRNUzAwWkdFMkxUZ3laV1F0TWpCaE9USTJOekppWm1GbUlpd2lkSGx3WlNJNld5SldaWEpwWm1saFlteGxRM0psWkdWdWRHbGhiQ0lzSWxWdWFYWmxjbk5wZEhsRVpXZHlaV1VpWFN3aWFYTnpkV1Z5SWpwN0ltbGtJam9pWkdsa09tcDNhenBsZVVweVpFaHJhVTlwU2xCVE1VRnBURU5LYW1OdVdXbFBhVXBHV2tSSk1VNVVSVFZKYVhkcFlUSnNhMGxxYjJsaFZYQk9WWHBXYVdFeGNGZFRWM2gxV1RKYWVGZ3dlRzFZTVU0eFpVVnZlVk51VWxKT1ZXZ3lXVmh2TTJSR1pGRmlhMFp4VmxaV2EyTjVTWE5KYm1kcFQybEtSMWR0VWpKa01FMDBXVlZrYjFWdVpIaGxiR1IzWkVkV2NVMUZOV0ZhTTFJelYxVkdTazFXVGpWU2JXTjRZbFYwUlZKV1VsQmFia1pHU1c0d0luMHNJbWx6YzNWaGJtTmxSR0YwWlNJNklqSXdNalV0TURRdE1UZFVNRGM2TkRnNk5EWXVNelkxTkRrM01EZzJXaUlzSW1OeVpXUmxiblJwWVd4VGRXSnFaV04wSWpwN0ltbGtJam9pWkdsa09tcDNhenBsZVVweVpFaHJhVTlwU2xCVE1VRnBURU5LYW1OdVdXbFBhVXBHV2tSSk1VNVVSVFZKYVhkcFlUSnNhMGxxYjJsbFZsSlBWMFZ3UlZWR1NrVk5NRkp4VlVoa1dHRjZRVEJUUjNoSVZXeG9jMVpGYUhkUFZrNU9UbFJPTVdOSVozcFVSbkJLVEZkU2ExWlRTWE5KYm1kcFQybEtURmxWTUhwV01XczBUbTFrZUdSNlJuUlhiRkowVGtaTk5GWnJUVEJpUjNodVVWZFdTbFl5TlVOaFZHUjVZakJHZGxsVE1EVmpWVnAyU1c0d0lpd2laR1ZuY21WbElqcDdJblI1Y0dVaU9pSkNZV05vWld4dmNrUmxaM0psWlNJc0ltNWhiV1VpT2lKQ1lXTm9aV3h2Y2lCdlppQlRZMmxsYm1ObElHRnVaQ0JCY25SekluMTlMQ0psZUhCcGNtRjBhVzl1UkdGMFpTSTZJakl3TWpZdE1EUXRNVGRVTURjNk5EZzZORFl1TXpZMk1EQTJNemMzV2lKOUxDSnFkR2tpT2lKMWNtNDZkWFZwWkRvNE1HRTRNVGMzTVMwM09UZ3hMVFJrWVRZdE9ESmxaQzB5TUdFNU1qWTNNbUptWVdZaUxDSmxlSEFpT2pFM056WTBNVEl4TWpZc0ltbGhkQ0k2TVRjME5EZzNOakV5Tml3aWJtSm1Jam94TnpRME9EYzJNVEkyZlEuSVNPNmhxRmVNdnpBOE4tRXRKcEFYd2h2dWZTaWIxYk9FYTFWdGhhMWlpSGJHN1plTVFGVll6ZnJmbnBpRmJXM2ExaV9ySVRud3RCS2VLNkVVUGdUQkEiXX19.AUqf8cy4UgaEbL4-2dsJ1fkEf_xPRJkbBhOJJOJHw8XsV6xDAXFEaakw6IMyr47cpUr9C-O1nji4-tHjbocyCg",
"presentation_submission": {
"id": "8cu3ixcb4cm",
"definition_id": "8cu3ixcb4cm",
"descriptor_map": [
{
"id": "UniversityDegree",
"format": "jwt_vp",
"path": "$",
"path_nested": {
"id": "UniversityDegree",
"format": "jwt_vc_json",
"path": "$.verifiableCredential[0]",
"customParameters": {}
}
}
]
},
"state": "Ag9gyQkYknmI",
"customParameters": {}
},
"verificationResult": true,
"policyResults": {
"results": [
{
"credential": "VerifiablePresentation",
"policyResults": [
{
"policy": "signature",
"description": "Checks a JWT credential by verifying its cryptographic signature using the key referenced by the DID in `iss`.",
"is_success": true,
"result": {
"sub": "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoieVROWEpEUFJEM0RqUHdXazA0SGxHUlhsVEhwOVNNNTN1cHgzTFpJLWRkVSIsIngiOiJLYU0zV1k4NmdxdzFtWlRtNFM4VkM0bGxnQWVJV25CaTdyb0FvYS05cUZvIn0",
"nbf": 1744877360,
"iat": 1744877420,
"jti": "8cu3ixcb4cm",
"iss": "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoieVROWEpEUFJEM0RqUHdXazA0SGxHUlhsVEhwOVNNNTN1cHgzTFpJLWRkVSIsIngiOiJLYU0zV1k4NmdxdzFtWlRtNFM4VkM0bGxnQWVJV25CaTdyb0FvYS05cUZvIn0",
"nonce": "b16fd466-3849-4762-ada8-2cfe737daba5",
"aud": "http://host.docker.internal:7003/openid4vc/verify",
"vp": {
"@context": [
"https://www.w3.org/2018/credentials/v1"
],
"type": [
"VerifiablePresentation"
],
"id": "8cu3ixcb4cm",
"holder": "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoieVROWEpEUFJEM0RqUHdXazA0SGxHUlhsVEhwOVNNNTN1cHgzTFpJLWRkVSIsIngiOiJLYU0zV1k4NmdxdzFtWlRtNFM4VkM0bGxnQWVJV25CaTdyb0FvYS05cUZvIn0",
"verifiableCredential": [
"eyJraWQiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0pqY25ZaU9pSkZaREkxTlRFNUlpd2lhMmxrSWpvaWFVcE5VelZpYTFwV1NXeHVZMlp4WDB4bVgxTjFlRW95U25SUk5VaDJZWG8zZEZkUWJrRnFWVlZrY3lJc0luZ2lPaUpHV21SMmQwTTRZVWRvVW5keGVsZHdkR1ZxTUU1YVozUjNXVUZKTVZONVJtY3hiVXRFUlZSUFpuRkZJbjAjaUpNUzVia1pWSWxuY2ZxX0xmX1N1eEoySnRRNUh2YXo3dFdQbkFqVVVkcyIsInR5cCI6IkpXVCIsImFsZyI6IkVkRFNBIn0.eyJpc3MiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0pqY25ZaU9pSkZaREkxTlRFNUlpd2lhMmxrSWpvaWFVcE5VelZpYTFwV1NXeHVZMlp4WDB4bVgxTjFlRW95U25SUk5VaDJZWG8zZEZkUWJrRnFWVlZrY3lJc0luZ2lPaUpHV21SMmQwTTRZVWRvVW5keGVsZHdkR1ZxTUU1YVozUjNXVUZKTVZONVJtY3hiVXRFUlZSUFpuRkZJbjAiLCJzdWIiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0pqY25ZaU9pSkZaREkxTlRFNUlpd2lhMmxrSWpvaWVWUk9XRXBFVUZKRU0wUnFVSGRYYXpBMFNHeEhVbGhzVkVod09WTk5OVE4xY0hnelRGcEpMV1JrVlNJc0luZ2lPaUpMWVUwelYxazRObWR4ZHpGdFdsUnRORk00VmtNMGJHeG5RV1ZKVjI1Q2FUZHliMEZ2WVMwNWNVWnZJbjAiLCJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL2V4YW1wbGVzL3YxIl0sImlkIjoidXJuOnV1aWQ6ODBhODE3NzEtNzk4MS00ZGE2LTgyZWQtMjBhOTI2NzJiZmFmIiwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsIlVuaXZlcnNpdHlEZWdyZWUiXSwiaXNzdWVyIjp7ImlkIjoiZGlkOmp3azpleUpyZEhraU9pSlBTMUFpTENKamNuWWlPaUpGWkRJMU5URTVJaXdpYTJsa0lqb2lhVXBOVXpWaWExcFdTV3h1WTJaeFgweG1YMU4xZUVveVNuUlJOVWgyWVhvM2RGZFFia0ZxVlZWa2N5SXNJbmdpT2lKR1dtUjJkME00WVVkb1VuZHhlbGR3ZEdWcU1FNWFaM1IzV1VGSk1WTjVSbWN4YlV0RVJWUlBabkZGSW4wIn0sImlzc3VhbmNlRGF0ZSI6IjIwMjUtMDQtMTdUMDc6NDg6NDYuMzY1NDk3MDg2WiIsImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmp3azpleUpyZEhraU9pSlBTMUFpTENKamNuWWlPaUpGWkRJMU5URTVJaXdpYTJsa0lqb2llVlJPV0VwRVVGSkVNMFJxVUhkWGF6QTBTR3hIVWxoc1ZFaHdPVk5OTlROMWNIZ3pURnBKTFdSa1ZTSXNJbmdpT2lKTFlVMHpWMWs0Tm1keGR6RnRXbFJ0TkZNNFZrTTBiR3huUVdWSlYyNUNhVGR5YjBGdllTMDVjVVp2SW4wIiwiZGVncmVlIjp7InR5cGUiOiJCYWNoZWxvckRlZ3JlZSIsIm5hbWUiOiJCYWNoZWxvciBvZiBTY2llbmNlIGFuZCBBcnRzIn19LCJleHBpcmF0aW9uRGF0ZSI6IjIwMjYtMDQtMTdUMDc6NDg6NDYuMzY2MDA2Mzc3WiJ9LCJqdGkiOiJ1cm46dXVpZDo4MGE4MTc3MS03OTgxLTRkYTYtODJlZC0yMGE5MjY3MmJmYWYiLCJleHAiOjE3NzY0MTIxMjYsImlhdCI6MTc0NDg3NjEyNiwibmJmIjoxNzQ0ODc2MTI2fQ.ISO6hqFeMvzA8N-EtJpAXwhvufSib1bOEa1Vtha1iiHbG7ZeMQFVYzfrfnpiFbW3a1i_rITnwtBKeK6EUPgTBA"
]
}
}
}
]
},
{
"credential": "UniversityDegree",
"policyResults": [
{
"policy": "signature",
"description": "Checks a JWT credential by verifying its cryptographic signature using the key referenced by the DID in `iss`.",
"is_success": true,
"result": {
"iss": "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoiaUpNUzVia1pWSWxuY2ZxX0xmX1N1eEoySnRRNUh2YXo3dFdQbkFqVVVkcyIsIngiOiJGWmR2d0M4YUdoUndxeldwdGVqME5aZ3R3WUFJMVN5RmcxbUtERVRPZnFFIn0",
"sub": "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoieVROWEpEUFJEM0RqUHdXazA0SGxHUlhsVEhwOVNNNTN1cHgzTFpJLWRkVSIsIngiOiJLYU0zV1k4NmdxdzFtWlRtNFM4VkM0bGxnQWVJV25CaTdyb0FvYS05cUZvIn0",
"vc": {
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://www.w3.org/2018/credentials/examples/v1"
],
"id": "urn:uuid:80a81771-7981-4da6-82ed-20a92672bfaf",
"type": [
"VerifiableCredential",
"UniversityDegree"
],
"issuer": {
"id": "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoiaUpNUzVia1pWSWxuY2ZxX0xmX1N1eEoySnRRNUh2YXo3dFdQbkFqVVVkcyIsIngiOiJGWmR2d0M4YUdoUndxeldwdGVqME5aZ3R3WUFJMVN5RmcxbUtERVRPZnFFIn0"
},
"issuanceDate": "2025-04-17T07:48:46.365497086Z",
"credentialSubject": {
"id": "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoieVROWEpEUFJEM0RqUHdXazA0SGxHUlhsVEhwOVNNNTN1cHgzTFpJLWRkVSIsIngiOiJLYU0zV1k4NmdxdzFtWlRtNFM4VkM0bGxnQWVJV25CaTdyb0FvYS05cUZvIn0",
"degree": {
"type": "BachelorDegree",
"name": "Bachelor of Science and Arts"
}
},
"expirationDate": "2026-04-17T07:48:46.366006377Z"
},
"jti": "urn:uuid:80a81771-7981-4da6-82ed-20a92672bfaf",
"exp": 1776412126,
"iat": 1744876126,
"nbf": 1744876126
}
}
]
}
],
"time": "PT2.675756835S",
"policiesRun": 2
},
"customParameters": {}
}
```
**Verification Status Policies Response**
The response of the verification status call will contain the status of the verification policies applied to the
credential(s) presented by the user. The policy results will be in the following format:
```json
{
"verificationResult": true,
"policyResults": {
"results": [
{
"credential": "VerifiableDiploma",
"policies": [
{
"policy": "signature",
"is_success": true
}
]
}
]
}
}
```
The `verificationResult` field will be `true` if all policies were successful, otherwise it will be `false`.
The `policyResults` field will contain the results of the policies applied to each credential. The `credential` field
will contain the name of the credential, and the `policies` field will contain the results of the policies applied to
the credential. The `policy` field will contain the name of the policy, and the `is_success` field will contain the
result of the policy.
## Next Steps
**Note:**
If you enjoy our tools, please leave us a star ⭐ [on GitHub](https://github.com/walt-id/waltid-identity).
You can learn more about the the different APIs in detail below:
- [Wallet](https://docs.walt.id/community-stack/wallet){:target="_blank"}
- [Issuer](https://docs.walt.id/community-stack/issuer){:target="_blank"}
- [Verifier](https://docs.walt.id/community-stack/verifier){:target="_blank"}
--------------------------------------------------------------------------------
PAGE: Tutorial v2 (30 min)
URL: https://docs.walt.id/community-stack/home/tutorial-30-min-v2
RAW: https://docs.walt.id/community-stack/home/tutorial-30-min-v2.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Tutorial v2 (30 min)
Video: https://youtu.be/lWSF_S-ID9Y
In this 30 min tutorial, we will take a look at the **Issuer2**, **Wallet2**, and **Verifier2** APIs of the walt.id
Community Stack — the latest generation of the issuer, wallet, and verifier services, implementing
**[OID4VCI 1.0](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md)** and
**[OID4VP 1.0](https://docs.walt.id/concepts/data-exchange-protocols/openid4vp.md)**.
**Note:**
Still building against the original **Issuer**, **Wallet**, and **Verifier**? Use the
[v1 tutorial](https://docs.walt.id/community-stack/home/tutorial-30-min.md) instead.
The tutorial is split into three sections:
1. Create a wallet
2. Issue a credential into the wallet
3. Request & verify the credential from the wallet
With that, we are covering a complete digital credential use case end-to-end using the walt.id v2 API
components:
- [Issuer2 API](https://docs.walt.id/community-stack/issuer2/getting-started.md)
- [Wallet2 API](https://docs.walt.id/community-stack/wallet2/getting-started.md)
- [Verifier2 API](https://docs.walt.id/community-stack/verifier2/getting-started.md)
### The Credential
For this tutorial, we will issue ourselves an **Open Badge Credential** — a
[W3C Verifiable Credential](https://docs.walt.id/concepts/digital-credentials/verifiable-credentials-w3c.md) with a JWT signature
(`jwt_vc_json`), issued from a profile that ships out of the box with Issuer2, so there's no configuration to write
before your first credential.
**Note:**
If you are curious to learn more about other credential formats, go [here](https://docs.walt.id/concepts/digital-credentials.md).
Issuer2, Wallet2, and Verifier2 also support SD-JWT VC and mdoc (ISO 18013-5) — this tutorial sticks to W3C VC to
keep the walkthrough focused.
#### Example Open Badge Credential (as issued)
```json
{
"@context": [
"https://www.w3.org/ns/credentials/v2",
"https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json",
"https://purl.imsglobal.org/spec/ob/v3p0/extensions.json"
],
"id": "urn:uuid:6bc9b310-4ef2-4d0a-97e7-c577b4c944d4",
"type": ["VerifiableCredential", "OpenBadgeCredential"],
"issuer": {
"id": "did:jwk:...",
"name": "Jobs for the Future (JFF)",
"type": ["Profile"],
"url": "https://www.jff.org/"
},
"credentialSchema": [
{
"type": "1EdTechJsonSchemaValidator2019",
"id": "https://purl.imsglobal.org/spec/ob/v3p0/schema/json/ob_v3p0_achievementcredential_schema.json"
}
],
"credentialSubject": {
"id": "did:key:...",
"type": ["AchievementSubject"],
"achievement": {
"id": "https://example.com/achievements/21st-century-skills/teamwork",
"type": ["Achievement"],
"name": "Teamwork",
"description": "This badge recognizes the development of the capacity to collaborate within a group environment.",
"criteria": {
"narrative": "Team members are nominated for this badge by their peers and recognized upon review by Example Corp management."
}
}
},
"validFrom": "2026-07-22T12:50:10.122342336Z"
}
```
---
## Setup
To bring up Issuer2, Wallet2, and Verifier2 locally, we'll use our
[docker compose](https://github.com/walt-id/waltid-identity/tree/main/docker-compose) setup.
**Clone [walt.id identity](https://github.com/walt-id/waltid-identity)**
```bash
git clone https://github.com/walt-id/waltid-identity.git && cd waltid-identity/docker-compose
```
**Launch the services**
```bash
docker compose up
```
#### Access APIs
- **Issuer2 API** — [http://localhost:7005/](http://localhost:7005/)
- **Verifier2 API** — [http://localhost:7004/](http://localhost:7004/)
- **Wallet2 API** — [http://localhost:7006/](http://localhost:7006/)
---
## Create a Wallet
By default, Wallet2's `auth` feature is off — there's no account, no registration, no login, and no bearer token to
manage. A wallet is just a container for keys, a DIDs, and credentials, and you get its ID back the moment you create
it. Authentication can be enabled — see [Authentication](https://docs.walt.id/community-stack/wallet2/configurations/config-files/auth.md).
A new wallet doesn't have any key or DID yet. The **key** is what the wallet signs with; the **DIDs** are
identifier derived from that key, which credentials get bound to when receiving them (new to DIDs? see
[Decentralised Identifiers](https://docs.walt.id/concepts/decentralised-identifiers.md)). We generate both next, as two explicit calls
right after creating the wallet.
### Create the Wallet
**Option: CURL**
**Endpoint:** `POST /wallet` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)
```bash
curl -X POST http://localhost:7006/wallet \
-H 'Content-Type: application/json' \
-d '{}'
```
**Body Parameters**
An empty body is enough — all fields are optional for the default, fully-managed wallet. See
[Managing Wallets](https://docs.walt.id/community-stack/wallet2/wallets.md) for `noDidStore` and the store-less-wallet options.
**Example Response**
```json
{
"walletId": "39c44fcf-63bf-41fb-a7de-d99d09458940"
}
```
**Note:**
Save the `walletId` — every subsequent call is scoped to it.
### Generate a Key
**Option: CURL**
**Endpoint:** `POST /wallet/{walletId}/keys/generate` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)
```bash
curl -X POST http://localhost:7006/wallet/{walletId}/keys/generate \
-H 'Content-Type: application/json' \
-d '{ "backend": "jwk", "keyType": "Ed25519" }'
```
**Body Parameters**
- `backend`: _String_ (required) - Discriminator for where the key is generated/stored. Use `jwk` for a
software-managed key (no external KMS required). Other supported values (`tse`, `azure-rest-api`, `oci-rest-api`,
`aws-rest-api`) require an additional `config` object for that backend — see
[Key Management](https://docs.walt.id/community-stack/wallet2/key-management).
- `keyType`: _String_ (optional) - One of `Ed25519`, `secp256r1`, `secp256k1` `RSA`. Defaults to `secp256r1`.
**Example Response**
```json
{
"keyId": "onxChakLCfOVM_YyZOOJ09fO1hgPrXxKxiBIPwTeack",
"keyType": "Ed25519"
}
```
This is the wallet's first (and so far only) key — it automatically becomes the **default key**, used for signing
whenever a later call doesn't specify `keyId` explicitly.
### Create a DID
W3C VCs bind the credential subject to a DID, not just a key, so we need one before we can receive one.
**Option: CURL**
**Endpoint:** `POST /wallet/{walletId}/dids/create` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)
```bash
curl -X POST http://localhost:7006/wallet/{walletId}/dids/create \
-H 'Content-Type: application/json' \
-d '{ "method": "key" }'
```
**Body Parameters**
- `method`: _String_ (required) - The DID method to create. Supported: `key`, `jwk`.
- `keyId`: _String_ (optional) - Key to derive the DID from. Defaults to the wallet's default key — the one we just
generated.
**Example Response**
```json
{
"did": "did:key:z6MksLjzRVB47ciKofd8AmNNrtvjrEd5DfTHUsUAQtFmiimD",
"document": {
"@context": ["https://www.w3.org/ns/did/v1", "https://w3id.org/security/suites/jws-2020/v1"],
"id": "did:key:z6MksLjzRVB47ciKofd8AmNNrtvjrEd5DfTHUsUAQtFmiimD",
"verificationMethod": [
{
"id": "did:key:z6MksLjz...#z6MksLjz...",
"type": "JsonWebKey2020",
"controller": "did:key:z6MksLjzRVB47ciKofd8AmNNrtvjrEd5DfTHUsUAQtFmiimD",
"publicKeyJwk": { "kty": "OKP", "crv": "Ed25519", "x": "..." }
}
],
"assertionMethod": ["did:key:z6MksLjz...#z6MksLjz..."],
"authentication": ["did:key:z6MksLjz...#z6MksLjz..."]
}
}
```
This is now the wallet's **default DID** too. Both defaults mean every call from here on can omit `keyId` and `did`
entirely.
---
## Issue Credential
### Confirm the Profile Is Loaded (optional)
Issuer2 issues from **[profiles](https://docs.walt.id/community-stack/issuer2/credential-profiles/overview.md)** — reusable credential
configurations declared ahead of time (issuer key, issuer DID, credential template, etc.). `openBadgeCredential`
ships pre-configured:
**Option: CURL**
**Endpoint:** `GET /issuer2/profiles/{profileId}` | [API Reference](https://docs.walt.id/community-stack/issuer2/full-api-reference.md)
```bash
curl http://localhost:7005/issuer2/profiles/openBadgeCredential
```
**Example Response** (shortened)
```json
{
"profileId": "openBadgeCredential",
"name": "OpenBadgeCredential",
"credentialConfigurationId": "OpenBadgeCredential_jwt_vc_json",
"issuerKey": { "type": "jwk", "jwk": { "kty": "EC", "crv": "P-256", "...": "..." } },
"issuerDid": "did:jwk:...",
"credentialData": {
"@context": ["https://www.w3.org/ns/credentials/v2", "..."],
"type": ["VerifiableCredential", "OpenBadgeCredential"],
"credentialSubject": { "achievement": { "name": "Teamwork", "...": "..." } }
},
"mapping": {
"id": "",
"issuer": { "id": "" },
"credentialSubject": { "id": "" },
"validFrom": ""
}
}
```
### Create a Credential Offer
Now let's actually issue a credential from that profile. This call creates a **credential offer** — a one-time,
short-lived pointer to a not-yet-issued credential that a wallet can redeem.
**Option: CURL**
**Endpoint:** `POST /issuer2/credential-offers` | [API Reference](https://docs.walt.id/community-stack/issuer2/full-api-reference.md)
```bash
curl -X POST http://localhost:7005/issuer2/credential-offers \
-H 'Content-Type: application/json' \
-d '{
"profileId": "openBadgeCredential",
"authMethod": "PRE_AUTHORIZED"
}'
```
**Body Parameters**
- `profileId`: _String_ (required) - The profile to issue from.
- `authMethod`: _String_ (required) - `PRE_AUTHORIZED` requires no user login and is the simplest way to get
started. Issuer2 also supports `AUTHORIZED` (authorization-code flow against an external IdP) — see
[Authorization Code Flow](https://docs.walt.id/community-stack/issuer2/protocol-flows/authorization-code-flow.md).
**Example Response**
```json
{
"offerId": "e124ba49-df08-45e1-8caa-810a712122e6",
"profileId": "openBadgeCredential",
"authMethod": "PRE_AUTHORIZED",
"expiresAt": 1784724878505,
"credentialOffer": "openid-credential-offer://?credential_offer_uri=http%3A%2F%2Fhost.docker.internal%3A7005%2Fopenid4vci%2Fcredential-offer%3Fid%3De124ba49-df08-45e1-8caa-810a712122e6"
}
```
`credentialOffer` is the [OID4VCI](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md) offer URL — hand it directly to
Wallet2 in the next step. Notice it already resolves
via `host.docker.internal`, which every container in the compose network can reach, so it works whether the caller is
your terminal, the demo wallet, or (as we'll do next) another container.
---
## Receive Credential in Wallet
This single call looks up the offer's details, signs a proof that the wallet owns the key the credential will be bound to,
then fetches the credential and stores it.
**Option: CURL**
**Endpoint:** `POST /wallet/{walletId}/credentials/receive` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)
```bash
curl -X POST http://localhost:7006/wallet/{walletId}/credentials/receive \
-H 'Content-Type: application/json' \
-d '{
"offerUrl": "openid-credential-offer://?credential_offer_uri=http%3A%2F%2Fhost.docker.internal%3A7005%2Fopenid4vci%2Fcredential-offer%3Fid%3De124ba49-df08-45e1-8caa-810a712122e6"
}'
```
**Body Parameters**
- `offerUrl`: _String_ - The `credentialOffer` value from the previous step.
**Example Response**
```json
{
"credentialIds": ["3d14e4b9-9478-41ed-bb52-bcf76b7dd36c"],
"deferredTransactionIds": {}
}
```
### Confirm It Was Stored
**Option: CURL**
**Endpoint:** `GET /wallet/{walletId}/credentials` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)
```bash
curl http://localhost:7006/wallet/{walletId}/credentials
```
**Example Response**
```json
[
{
"id": "3d14e4b9-9478-41ed-bb52-bcf76b7dd36c",
"format": "jwt_vc_json",
"issuer": "did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2Ii...",
"subject": "did:key:z6MksLjzRVB47ciKofd8AmNNrtvjrEd5DfTHUsUAQtFmiimD",
"addedAt": "2026-07-22T12:50:10.138097127Z"
}
]
```
🎉 You've received an Open Badge Credential into the wallet. To see the full parsed contents (not just metadata), use
`GET /wallet/{walletId}/credentials/{credentialId}` — see
[Credential Management](https://docs.walt.id/community-stack/wallet2/credential-management.md).
---
## Request & Verify Credential
We now switch to Verifier2 to build an [OID4VP](https://docs.walt.id/concepts/data-exchange-protocols/openid4vp.md) authorization request.
Verifier2 uses
**[DCQL](https://docs.walt.id/concepts/data-exchange-protocols/dcql.md)** (Digital Credentials Query Language) to describe the
credentials it wants — the credential type/format is expressed as a `dcql_query` directly in the request body.
**Option: CURL**
**Endpoint:** `POST /verification-session/create` | [API Reference](https://docs.walt.id/community-stack/verifier2/full-api-reference.md)
```bash
curl -X POST http://localhost:7004/verification-session/create \
-H 'Content-Type: application/json' \
-d '{
"flow_type": "cross_device",
"core_flow": {
"dcql_query": {
"credentials": [
{
"id": "credential_1",
"format": "jwt_vc_json",
"meta": { "type_values": [["VerifiableCredential", "OpenBadgeCredential"]] }
}
]
},
"policies": {
"vc_policies": [{ "policy": "signature" }]
}
}
}'
```
**Body Parameters**
- `flow_type`: _String_ (required) - `cross_device` (QR code), `same_device` (deep link), `dc_api` / `dc_api-annex-c`
(browser-native Digital Credentials API). We use `cross_device` here; it needs no extra flow-specific fields.
- `core_flow.dcql_query.credentials`: _Array_ (required) - one entry per credential to request. `id` is a
free-choice label used later to key policy results; `format` + `meta.type_values` describe what to accept — here,
any credential whose `type` array matches `["VerifiableCredential", "OpenBadgeCredential"]`.
- `core_flow.policies.vc_policies`: _Array_ (optional) - validation rules applied to the received credential. See
[Policies](https://docs.walt.id/community-stack/verifier2/policies/overview.md) for the full list.
**Omitting `policies` entirely defaults to signature-only verification** — the same as what we set explicitly here.
**Example Response**
```json
{
"sessionId": "d20e3de1-0695-4c54-bb47-4104d85c41d7",
"bootstrapAuthorizationRequestUrl": "openid4vp://authorize?client_id=redirect_uri%3Ahttp%3A%2F%2Fhost.docker.internal%3A7004%2Fverification-session%2Fd20e3de1-0695-4c54-bb47-4104d85c41d7%2Fresponse&request_uri=http%3A%2F%2Fhost.docker.internal%3A7004%2Fverification-session%2Fd20e3de1-0695-4c54-bb47-4104d85c41d7%2Frequest",
"fullAuthorizationRequestUrl": "openid4vp://authorize?response_type=vp_token&client_id=redirect_uri%3Ahttp%3A%2F%2Fhost.docker.internal%3A7004%2Fverification-session%2Fd20e3de1-0695-4c54-bb47-4104d85c41d7%2Fresponse&state=1c7706bd-8e23-41a6-8bd9-291c51e8010a&response_mode=direct_post&nonce=1ccec52e-f0d0-48fc-b51e-fc1115c5d930&response_uri=http%3A%2F%2Fhost.docker.internal%3A7004%2Fverification-session%2Fd20e3de1-0695-4c54-bb47-4104d85c41d7%2Fresponse&dcql_query=%7B%22credentials%22%3A%5B%7B%22id%22%3A%22credential_1%22%2C%22format%22%3A%22jwt_vc_json%22%2C%22meta%22%3A%7B%22type_values%22%3A%5B%5B%22VerifiableCredential%22%2C%22OpenBadgeCredential%22%5D%5D%7D%7D%5D%7D&client_metadata=..."
}
```
- `sessionId`: use this to check the result later.
- `bootstrapAuthorizationRequestUrl`: the short, `request_uri`-by-reference form — this is what we hand to the
wallet next (and what you'd turn into a QR code for a real cross-device flow).
---
## Present Credential
A single call resolves the verifier's request, runs the DCQL match against the wallet's stored credentials, signs
with the default key/DID, and submits the response.
**Option: CURL**
**Endpoint:** `POST /wallet/{walletId}/credentials/present` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)
```bash
curl -X POST http://localhost:7006/wallet/{walletId}/credentials/present \
-H 'Content-Type: application/json' \
-d '{
"requestUrl": "openid4vp://authorize?client_id=verifier2&request_uri=http%3A%2F%2Fhost.docker.internal%3A7004%2Fverification-session%2Fd20e3de1-0695-4c54-bb47-4104d85c41d7%2Frequest"
}'
```
**Body Parameters**
- `requestUrl`: _String_ - the `bootstrapAuthorizationRequestUrl` (or `fullAuthorizationRequestUrl`) from the
previous step.
**Example Response**
```json
{
"transmission_success": true,
"verifier_response": {
"status": "received",
"message": "Presentation received and is being processed."
}
}
```
**Note:**
`transmission_success: true` only confirms the wallet delivered the presentation — it says nothing about whether the
credential actually passed verification. Check that on the verifier side next.
---
## Check the Verification Result
Back on Verifier2, poll (or subscribe via [SSE/callback](https://docs.walt.id/community-stack/verifier2/callback-and-sse.md)) the session to
see the final status and policy results.
**Option: CURL**
**Endpoint:** `GET /verification-session/{sessionId}/info` | [API Reference](https://docs.walt.id/community-stack/verifier2/full-api-reference.md)
```bash
curl http://localhost:7004/verification-session/{sessionId}/info
```
**Example Response** (shortened)
```json
{
"id": "d20e3de1-0695-4c54-bb47-4104d85c41d7",
"status": "SUCCESSFUL",
"attempted": true,
"reattemptable": true,
"policy_results": {
"vp_policies": {
"credential_1": {
"jwt_vc_json/audience-check": { "success": true },
"jwt_vc_json/nonce-check": { "success": true },
"jwt_vc_json/envelope_signature": { "success": true }
}
},
"vc_policies": [
{
"policy": { "policy": "signature", "id": "signature" },
"success": true,
"result": {
"verification_result": true,
"verified_data": {
"iss": "did:jwk:...",
"sub": "did:key:z6MksLjz...",
"type": ["VerifiableCredential", "OpenBadgeCredential"],
"credentialSubject": {
"id": "did:key:z6MksLjz...",
"achievement": { "name": "Teamwork", "...": "..." }
}
}
}
}
],
"overallSuccess": true
},
"presented_credentials": {
"credential_1": [
{ "type": "vc-w3c_2", "credentialData": { "...": "the presented credential" } }
]
}
}
```
`status: "SUCCESSFUL"` and `policy_results.overallSuccess: true` confirm the credential was received and the
signature policy passed. 🎉
You've issued, received, requested, and verified an Open Badge Credential end-to-end
using Issuer2, Wallet2, and Verifier2.
---
## Next Steps
**Note:**
If you enjoy our tools, please leave us a star ⭐ [on GitHub](https://github.com/walt-id/waltid-identity).
You can learn more about the different APIs in detail below:
- [Issuer2](https://docs.walt.id/community-stack/issuer2/getting-started.md){:target="_blank"}
- [Wallet2](https://docs.walt.id/community-stack/wallet2/getting-started.md){:target="_blank"}
- [Verifier2](https://docs.walt.id/community-stack/verifier2/getting-started.md){:target="_blank"}
--------------------------------------------------------------------------------
PAGE: What is the Community Stack
URL: https://docs.walt.id/community-stack/home/the-community-stack
RAW: https://docs.walt.id/community-stack/home/the-community-stack.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# What is the Community Stack
The walt.id Community Stack is **a set of open-source tools designed for developers and organizations
to build robust end-to-end identity and wallet solutions** across industries and ecosystems. It is designed to
facilitate
the development and deployment of holistic identity solutions, ensuring modularity, interoperability, and ease of use.
## Architecture and Structure
The walt.id Community Stack provides all the necessary functionalities to
launch identity solutions seamlessly. **Its architecture can be broadly categorized into multi-platform libraries,
services and APIs, Applications and external services and integrations** as shown below in the architecture image.

### Multi-Platform Libraries
The multi-platform libraries based on Kotlin Multiplatform bring **support for a variety of programming languages
including
Kotlin/Java, JavaScript and more in the future**. These libraries allow mix and matching of identity features,
credentials
formats, and ecosystem functionalities to ensure a compact, concise and performant codebase.
#### Core Libraries:
- [**Crypto**](https://github.com/walt-id/waltid-identity/tree/main/waltid-libraries/crypto/waltid-crypto): Manages keypair and
signature functionalities.
- [**SD-JWT**](https://github.com/walt-id/waltid-identity/tree/main/waltid-libraries/sdjwt/waltid-sdjwt): Issues [Selective
Disclosure JWTs (SD-JWT)](/concepts/digital-credentials/sd-jwt-vc).
- [**DIDs**](https://github.com/walt-id/waltid-identity/tree/main/waltid-libraries/waltid-did): Manages [Decentralized
Identifiers (DIDs)](/concepts/decentralised-identifiers).
- [**OpenID4VC**](https://github.com/walt-id/waltid-identity/tree/main/waltid-libraries/protocols/waltid-openid4vc): Facilitates
[OID4VCI](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md)/[OID4VP](https://docs.walt.id/concepts/data-exchange-protocols/openid4vp.md) protocol issuance and verification.
#### Credential Libraries:
- [**Verifiable Credentials**](https://github.com/walt-id/waltid-identity/tree/main/waltid-libraries/credentials/waltid-w3c-credentials): Issues and
verifies [W3C Credentials](https://docs.walt.id/concepts/digital-credentials.md) with corresponding signatures (JWT, SD-JWT, JSON-LD).
- [**mdoc Credentials**](https://github.com/walt-id/waltid-identity/tree/main/waltid-libraries/credentials/waltid-mdoc-credentials):
Creates and verifies [mdoc credentials (mDL)](https://docs.walt.id/concepts/digital-credentials/mdoc-mdl-iso.md)
### Services and APIs
The services and APIs are designed to enhance applications with Issuer, Verifier, and ID wallet capabilities.
- [**Issuer API**](https://docs.walt.id/community-stack/issuer/getting-started.md): Allows applications to issue credentials (VCs,
mdocs) and tokens (NFTs, SBTs) across ecosystems.
- [**Verifier API**](https://docs.walt.id/community-stack/verifier/getting-started.md): Enables applications to verify credentials or
tokens based on a variety of verification policies.
- [**Wallet API**](https://docs.walt.id/community-stack/wallet/getting-started.md): Extends applications with identity wallet
capabilities such as collecting, storing, managing, and
sharing identity credentials or tokens.
### Applications
To ensure a faster go to market or use-case development, the walt.id Community Stack includes customizable white-label
applications.
- [**Web Issuer**](https://docs.walt.id/community-stack/issuer/apps/portal/overview.md): A web
portal or claim page supporting protocols like OIDC4VC.
- [**Web Verifier**](https://docs.walt.id/community-stack/verifier/apps/portal/overview.md): A web
portal or verification page supporting protocols like OIDC4VP.
- [**Wallet**](https://docs.walt.id/community-stack/wallet/apps/web-wallet/overview.md): A custodial
web-wallet for collecting, managing, and sharing credentials (VCs, mdocs) and displaying tokens (NFTs,
SBTs) based on a progressive web app (PWA) that works across devices.
### External Services & Integrations
The Community Stack is built to be interoperable with third-party services and tools like:
- KMS/Hardware Keystores
- Trust Registries
- TSP Signature providers
- Various data storage options
The openness of the stack allows developers and organizations to enhance the stack by integrating additional external key storage
solutions, trust service providers, and identity verification services not already supported by default.
## Next steps
- [Issuer](https://docs.walt.id/community-stack/issuer) - enable apps to issue credentials (W3C JWTs and SD-JWTs) via OID4VC.
- [Verifier](https://docs.walt.id/community-stack/verifier) - enable apps to verify credentials (W3C JWTs and SD-JWTs) via OID4VP/SIOPv2.
- [Wallet](https://docs.walt.id/community-stack/wallet) - extend apps with wallet capabilities to collect, store, manage and share identity credentials and tokens.
--------------------------------------------------------------------------------
PAGE: FAQs
URL: https://docs.walt.id/community-stack/home/faq
RAW: https://docs.walt.id/community-stack/home/faq.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Frequently Asked Questions
This page addresses common questions about the walt.id Community Stack, its components, setup, and usage.
## General Questions
**Q: What is the walt.id Community Stack?**
A: The walt.id Community Stack is a set of open-source tools designed for developers and organizations to build robust end-to-end digital identity and wallet solutions across industries and ecosystems. It provides all the necessary functionalities to launch identity solutions (issuer, verifier, and wallet) seamlessly, including multi-platform libraries, services and APIs, and white-label applications.
**Q: What's the difference between the Community Stack and Enterprise Stack?**
A: The Community Stack is open-source and designed to build simple end-to-end digital ID solutions in various environments using APIs, SDKs, and white-label apps. Its API services are stateless and typically deployed as single instances via Docker. The SDKs, written in Kotlin and available for Java, enable direct integration of digital ID features. The Enterprise Stack builds on top of the Community Stack and adds enterprise capabilities such as multi-tenancy support, built-in GUI management tools, lifecycle management, credential revocation mechanisms, logs, audits, pre-built integrations, and more. The Enterprise Stack is ideal for large organizations, governments, and businesses offering digital identity platforms or SaaS solutions that need to scale. Learn [here](https://docs.walt.id/enterprise-stack/home.md).
**Q: What are the main components of the Community Stack?**
A: The Community Stack consists of:
- **Multi-Platform Libraries**: Core libraries (Crypto, SD-JWT, DIDs, OpenID4VC), credential libraries (W3C Verifiable Credentials, Mobile Driver's Licenses (ISO/IEC 18013-5 mDLs)), and ecosystem libraries
- **Services and APIs**: [Issuer API](https://docs.walt.id/community-stack/issuer/getting-started.md), [Verifier API](https://docs.walt.id/community-stack/verifier/getting-started.md), and [Wallet API](https://docs.walt.id/community-stack/wallet/getting-started.md)
- **Applications**: White-label web applications (Web Issuer, Web Verifier, Web Wallet)
- **External Services & Integrations**: Support for KMS/Hardware Keystores
**Q: What credential formats does the Community Stack support?**
A: The Community Stack supports multiple credential formats including:
- [**SD-JWT VC (IETF)**](https://docs.walt.id/concepts/digital-credentials/sd-jwt-vc.md): Selective Disclosure JSON Web Token Verifiable Credentials
- [**W3C Verifiable Credentials**](https://docs.walt.id/concepts/digital-credentials/verifiable-credentials-w3c.md): Versions 1.1+ and 2.0 with JSON Web signatures
- [**ISO 18013-5 mDL**](https://docs.walt.id/concepts/digital-credentials/mdoc-mdl-iso.md): Mobile Driver's License format
- **Custom formats**: The system is extensible to support new or proprietary formats
**Q: Which protocols and standards are supported?**
A: The Community Stack supports:
- [**OID4VCI**](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md): Draft 11, Draft 13, and V1 for credential issuance
- [**OID4VP**](https://docs.walt.id/concepts/data-exchange-protocols/openid4vp.md): Draft 14, 20, and V1 for credential verification
- **ISO/IEC 18013-7**: For mobile driver's license online exchange
- **Multiple [DID](https://docs.walt.id/concepts/decentralised-identifiers.md) methods**: did:key, did:jwk, did:web, did:cheqd, and more
## Architecture & Components
**Q: How does the Community Stack architecture work?**
A: The Community Stack architecture is categorized into:
1. **Multi-platform libraries** based on Kotlin Multiplatform supporting Kotlin/Java, JavaScript, and more
2. **Services and APIs** that enhance applications with Issuer, Verifier, and ID wallet capabilities
3. **Applications** providing customizable white-label solutions
4. **External services & integrations** for interoperability with third-party tools
**Q: What are the three main APIs and what do they do?**
A: The three main APIs are:
- **Issuer API**: Allows applications to issue credentials (VCs, mdocs) across ecosystems via OID4VCI
- **Verifier API**: Enables applications to verify credentials (VCs, mdocs) based on various verification policies via OID4VP
- **Wallet API**: Extends applications with identity wallet capabilities for collecting, storing, managing, and sharing identity credentials
**Q: What white-label applications are available?**
A: The Community Stack includes:
- **Web Issuer**: A web portal supporting OIDC4VC for credential issuance
- **Web Verifier**: A web portal supporting OIDC4VP for credential verification
- **Web Wallet**: A custodial web-wallet (PWA) for collecting, managing, and sharing credentials
## Setup & Deployment
**Q: How do I get started with the Community Stack?**
A: You can get started in several ways:
1. **Quick Start**: Use our [docker compose](https://github.com/walt-id/waltid-identity/tree/main/docker-compose) to spin up all services locally
2. **Individual APIs**: Set up specific APIs (Issuer, Verifier, Wallet) using Docker or open-source installation
3. **White-label Apps**: Use the pre-built web applications for immediate testing
4. **SDKs**: Integrate the Kotlin/Java libraries directly into your applications
**Q: What are the system requirements for running the Community Stack?**
A: The Community Stack requires:
- **Docker**: For containerized deployment
- **Java/Kotlin**: For SDK usage
- **Node.js**: For web applications (if running locally)
**Q: Can I run the Community Stack without Docker?**
A: Yes, you can run individual components without Docker:
- **APIs**: Build from source using Gradle/Maven
- **Web Apps**: Run locally with Node.js
- **SDKs**: Include as dependencies in your Kotlin/Java projects
**Q: How do I access the services once they're running?**
A: When running via docker-compose, you can access:
- **Issuer API**: [Local](http://localhost:7002/) | [Deployed](https://issuer.demo.walt.id/swagger/index.html)
- **Verifier API**: [Local](http://localhost:7003/) | [Deployed](https://verifier.demo.walt.id/swagger/index.html)
- **Wallet API**: [Local](http://localhost:7001/) | [Deployed](https://wallet.demo.walt.id/swagger/index.html#/)
- **Web Wallet**: [Local](http://localhost:7101/) | [Deployed](https://wallet.demo.walt.id/login)
- **Web Portal**: [Local](http://localhost:7102/) | [Deployed](https://portal.walt.id/)
## Key Management & Security
**Q: How does key management work in the Community Stack?**
A: The Community Stack is KMS-agnostic and supports:
- **External KMS**: AWS KMS, Azure Key Vault, Google Cloud KMS, Hashicorp Vault, OCI KMS
- **Raw key material**: For development and PoC environments
- **Multiple algorithms**: ed25519, secp256k1, secp256r1, and RSA
**Q: What cryptographic algorithms are supported?**
A: The Community Stack supports multiple asymmetric key types including ed25519, secp256k1, secp256r1, and RSA.
**Q: How do I integrate with my existing KMS infrastructure?**
A: The system supports multiple authentication methods:
- **Cloud environments**: Use automatically assigned roles (like AWS IAM roles) for keyless authentication
- **Other KMS providers**: Use API keys for authentication
- **Custom integrations**: Extend the system to support your specific KMS requirements
## Credential Management
**Q: How can I collect and manage credential data?**
A: There are three main approaches:
1. **Before credential offer creation**: Collect all data upfront from external sources (CRM, databases, registries)
2. **After offer creation, before signing**: Use [data functions](https://docs.walt.id/community-stack/issuer/credential-issuance/data-functions.md) to dynamically enrich credentials with timestamps, DIDs, or external API calls
3. **During user authentication**: Pull verified claims from identity providers during the OID4VCI authorization flow
**Q: How do I handle credential revocation and status management?**
A: In the Community Stack, you can issue credentials with status information, but you're responsible for hosting and managing the status credentials externally. The Enterprise Stack includes built-in credential status management with support for standards like Bitstring Status List, Token Status List, and StatusList2021.
**Q: Can I set expiration dates on credentials?**
A: Yes, credentials can include expiration and "valid from" dates. These can be set explicitly during credential creation or generated dynamically using timestamp data functions. This is useful for time-limited credentials like event tickets, student IDs, or transportation passes.
## Integration & Development
**Q: How do I integrate the Community Stack with my existing systems?**
A: The Community Stack is designed for interoperability:
- **APIs**: RESTful APIs for easy integration
- **SDKs**: Kotlin/Java libraries for direct integration
- **External services**: Support for existing KMS, identity providers, and data storage
**Q: What programming languages are supported?**
A: The Community Stack supports:
- **Kotlin/Java**: Primary SDK support
- **Any language**: Via REST APIs
- **Future support**: Additional languages planned
**Q: How do I customize credential appearance in wallets?**
A: Credential branding can be configured in two ways:
1. **Issuer metadata**: Define consistent styling per credential type (background color, logo, description)
2. **Embedded branding**: Include visual attributes directly in individual credentials for personalized presentation
**Q: What if I need to support a new credential format not currently supported?**
A: The walt.id system is built for extensibility. New credential formats can be added as dedicated Kotlin libraries and integrated through the shared digital-credential abstraction layer. Contact us at tamino@walt.id for more information.
## Troubleshooting
**Q: The services won't start. What should I check?**
A: Common issues include:
- **Docker not running**: Ensure Docker is installed and running
- **Port conflicts**: Check if ports 7001, 7002, 7003, 7101, 7102 are available
- **Configuration files**: Verify config files are properly mounted and formatted
**Q: How do I monitor the health of my Community Stack deployment?**
A: The Community Stack provides:
- **Health check endpoints**: `/livez` endpoints on all APIs
- **Docker logs**: Container-level logging for deployment issues
## Compliance & Standards
**Q: Is the Community Stack compliant with [eIDAS 2.0](https://walt.id/eidas2) requirements?**
A: The Community Stack is continiously aligned with the EU's emerging digital ID ecosystem under [eIDAS 2.0](https://walt.id/eidas2) (electronic IDentification, Authentication and trust Services), including ARF and implementing acts. This ensures credentials can be issued to different wallets, including EUDI wallets.
## Ecosystem Support
**Q: What identity ecosystems does the Community Stack support?**
A: The Community Stack supports multiple ecosystems including:
- **[eIDAS 2.0](https://walt.id/eidas2)**: EU digital identity framework
- **EBSI**: European Blockchain Services Infrastructure
- **cheqd**: Decentralized identity network
- **iota**: Distributed ledger technology
- **Custom ecosystems**: Extensible for proprietary or new ecosystems
**Q: How do I integrate with a specific ecosystem?**
A: Ecosystem integration typically involves:
1. **Trust registry setup**: Configure trust registries for the ecosystem
2. **DID method configuration**: Set up appropriate DID methods
3. **Credential schema registration**: Register credential schemas with the ecosystem
4. **Compliance verification**: Ensure compliance with ecosystem requirements
## Support & Community
**Q: Where can I get help with the Community Stack?**
A: Several resources are available:
- **Documentation**: Comprehensive guides and API references
- **GitHub**: [walt.id identity repository](https://github.com/walt-id/waltid-identity) for issues and discussions
- **Community**: [Join our community channel](https://walt.id/discord) for peer support
**Q: How do I contribute to the Community Stack?**
A: Contributions are welcome:
- **GitHub**: Submit issues, feature requests, and pull requests
- **Testing**: Test and report bugs or compatibility issues
- **Community**: Share use cases and best practices
---
### Still Have Questions?
If you couldn't find the answer to your question, please:
- Check our [comprehensive documentation](https://docs.walt.id/community-stack/home.md)
- Visit our [GitHub repository](https://github.com/walt-id/waltid-identity)
- Contact us at [tamino@walt.id](mailto:tamino@walt.id) for specific inquiries.
--------------------------------------------------------------------------------
PAGE: Getting Started
URL: https://docs.walt.id/community-stack/verifier2/getting-started
RAW: https://docs.walt.id/community-stack/verifier2/getting-started.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Getting Started
**Verifier2** is an open-source HTTP API service that acts as the relying party in a digital credential exchange. It
sends a presentation request to a holder's wallet, receives the signed credential presentation in response, and
validates it against configurable policies. Credential exchange runs over [**OpenID for Verifiable Presentations (OID4VP)**](https://docs.walt.id/concepts/data-exchange-protocols/openid4vp.md) v1.0,
with additional support for the [Digital Credentials API](https://docs.walt.id/concepts/data-exchange-protocols/dc-api.md) and
[ISO/IEC 18013-7](https://docs.walt.id/concepts/data-exchange-protocols.md). Supported credential formats include [**W3C Verifiable Credential (W3C VC)**](https://docs.walt.id/concepts/digital-credentials/verifiable-credentials-w3c.md),
[**SD-JWT VC (IETF)**](https://docs.walt.id/concepts/digital-credentials/sd-jwt-vc.md), [ISO 18013-5 mDL](https://docs.walt.id/concepts/digital-credentials/mdoc-mdl-iso.md),
and ISO 23220 PhotoID.
**Note:**
The original verifier service, which supports Draft 14 and Draft 20 of the OID4VP specification, is deprecated. It remains available for existing integrations — see the existing
[Verifier documentation](https://docs.walt.id/community-stack/verifier/getting-started.md). For new projects, use **Verifier2** instead. If you're migrating from Verifier (v1), see the [Migration Guide](https://docs.walt.id/community-stack/verifier2/migration-from-verifier1.md).
## Supported Standards
Verifier2 supports the following standards:
| | |
|:-------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **Credential Formats:** | [SD-JWT VC (IETF)](https://docs.walt.id/concepts/digital-credentials/sd-jwt-vc.md), [W3C VC (v1.1+, v2.0)](https://docs.walt.id/concepts/digital-credentials/verifiable-credentials-w3c.md), [ISO 18013-5 mDL](https://docs.walt.id/concepts/digital-credentials/mdoc-mdl-iso.md), ISO 23220 PhotoID |
| **Credential Exchange:** | [OID4VP](https://docs.walt.id/concepts/data-exchange-protocols.md) (Version 1.0), [ISO/IEC 18013-7](https://docs.walt.id/concepts/data-exchange-protocols.md), [Digital Credentials API](https://docs.walt.id/concepts/data-exchange-protocols/dc-api.md) |
| **Signature Algorithms:** | ed25519, secp256k1, secp256r1, RSA |
## Setup
If you are new to the stack, the **Docker Compose quick start** is the fastest way to get running — it spins up the
Verifier, Issuer, and Wallet together in one command, which mirrors a real verification flow. Once running, the API
is available at `http://localhost:7004`.
**Note:**
Verifier2 listens on port `7004` by default, whether run standalone or via Docker Compose. The original Verifier (v1) runs on port `7003`, avoiding a conflict when both run together.
- [Docker Compose quick start](https://docs.walt.id/community-stack/home.md#quick-start) - Spin up the verifier API locally together with the other services. _(Recommended for first-time users)_
- [Run standalone with Docker](https://docs.walt.id/community-stack/verifier2/setup.md) - Pull and run the verifier container in isolation using the [walt.id identity repo](https://github.com/walt-id/waltid-identity.git). See the [configuration reference](https://docs.walt.id/community-stack/verifier2/configurations) for available config options.
- [Full API reference](https://docs.walt.id/community-stack/verifier2/full-api-reference.md) - Explore all available endpoints via Swagger.
## Features
- [Credential Verification](https://docs.walt.id/community-stack/verifier2/credential-verification/vc-oid4vp.md) - Request and verify various digital credential types and formats ([W3C](https://docs.walt.id/community-stack/verifier2/credential-verification/vc-oid4vp.md), [SD-JWT](https://docs.walt.id/community-stack/verifier2/credential-verification/sd-jwt-vc-oid4vp.md), [mDL](https://docs.walt.id/community-stack/verifier2/credential-verification/mdl-oid4vp.md)) via OID4VP.
- [Policies](https://docs.walt.id/community-stack/verifier2/credential-verification/policies/overview) - Apply built-in or custom policies to control validation checks such as expiry, schema conformance, and trust anchor verification.
- [Callbacks & SSE](https://docs.walt.id/community-stack/verifier2/callback-and-sse.md) - Get notified when a verification session completes via a webhook callback or a Server-Sent Events (SSE) stream, including the session status, policy results, and presented credentials.
## Start Verifying
Choose the credential type and exchange protocol that matches your use case:
- [W3C Verifiable Credentials via OID4VP](https://docs.walt.id/community-stack/verifier2/credential-verification/vc-oid4vp.md)
- [SD-JWT VC via OID4VP](https://docs.walt.id/community-stack/verifier2/credential-verification/sd-jwt-vc-oid4vp.md)
- [Mobile Driver's Licenses (mDL) via OID4VP](https://docs.walt.id/community-stack/verifier2/credential-verification/mdl-oid4vp.md)
- [Verifiable Credentials via Digital Credentials API](https://docs.walt.id/community-stack/verifier2/credential-verification/dc-api.md)
--------------------------------------------------------------------------------
PAGE: Setup
URL: https://docs.walt.id/community-stack/verifier2/setup
RAW: https://docs.walt.id/community-stack/verifier2/setup.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Setup
## Installation & Running the Project
Make sure you have Docker installed on your machine.
### Option 1: Using Docker Pull
**1. Clone the Identity Repo**
Clone the repository to your local machine. The repo contains the default configuration files that will be mounted into the container in the next step.
```bash
git clone https://github.com/walt-id/waltid-identity.git && cd waltid-identity
```
**2. Pull the Docker Image**
Pull the pre-built Docker image from Docker Hub:
```bash
docker pull waltid/verifier-api2
```
**3. Run the Docker Image**
Run the container, mounting the config directory from the cloned repository. This gives the service its default configuration — see the note below for details.
```bash
docker run -p 7004:7004 \
-itv $(pwd)/waltid-services/waltid-verifier-api2/config:/waltid-verifier-api2/config \
-t waltid/verifier-api2
```
The `-v` flag mounts `waltid-services/waltid-verifier-api2/config` from the cloned repository into the container. This directory contains the service configuration files (e.g. server port, trust store settings). You can edit these files before starting the container to customise the service behaviour.
**Note:**
See the [configuration reference](https://docs.walt.id/community-stack/verifier2/configurations/feature-manager.md) for a full description of all
available config files and their fields.
Once the container starts, the API is available at `http://localhost:7004`. You can confirm it is running by opening `http://localhost:7004/swagger` in your browser.
### Option 2: Building the Image Yourself
**Note:**
This option requires **Java (JDK 17+)**, **Gradle**, and **Docker** to be installed on your machine.
**1. Clone the Identity Repo**
Clone the repository to your local machine:
```bash
git clone https://github.com/walt-id/waltid-identity.git && cd waltid-identity
```
**2. Build the Docker Image**
If you prefer to build the Docker image yourself, use the Gradle Ktor/Jib tasks:
```bash
# Development (local Docker daemon, single-arch)
./gradlew :waltid-services:waltid-verifier-api2:publishImageToLocalRegistry
# image: waltid/verifier-api:
```
```bash
# Production (multi-arch push to your registry)
export DOCKER_USERNAME=
export DOCKER_PASSWORD=
./gradlew :waltid-services:waltid-verifier-api2:publishImage
# image: docker.io//verifier-api:
```
Note: multi-arch images require a registry push. Local tar output is single-arch only.
**3. Run the Docker Image**
After building the image, run it using (adjust the image name if you pushed to your registry):
```bash
docker run -p 7004:7004 \
-itv $(pwd)/waltid-services/waltid-verifier-api2/config:/waltid-verifier-api2/config \
-t waltid/verifier-api2
```
The `-v` flag mounts `waltid-services/waltid-verifier-api2/config` from the cloned repository into the container. This directory contains the service configuration files (e.g. server port, trust store settings). You can edit these files before starting the container to customise the service behaviour.
**Note:**
See the [configuration reference](https://docs.walt.id/community-stack/verifier2/configurations) for a full description of all
available config files and their fields.
Once the container starts, the API is available at `http://localhost:7004`. You can confirm it is running by opening `http://localhost:7004/swagger` in your browser.
## Start Verifying Credentials
Now that the Verifier2 API is running, choose the credential type that matches your use case:
- [W3C Verifiable Credentials via OID4VP](https://docs.walt.id/community-stack/verifier2/credential-verification/vc-oid4vp.md)
- [SD-JWT VC via OID4VP](https://docs.walt.id/community-stack/verifier2/credential-verification/sd-jwt-vc-oid4vp.md)
- [Mobile Driver's Licenses (mDL) via OID4VP](https://docs.walt.id/community-stack/verifier2/credential-verification/mdl-oid4vp.md)
- [Verifiable Credentials via Digital Credentials API](https://docs.walt.id/community-stack/verifier2/credential-verification/dc-api.md)
- [Full API reference](https://docs.walt.id/community-stack/verifier2/full-api-reference.md)
--------------------------------------------------------------------------------
PAGE: Verifying Verifiable Credentials (W3C) via OID4VP
URL: https://docs.walt.id/community-stack/verifier2/credential-verification/vc-oid4vp
RAW: https://docs.walt.id/community-stack/verifier2/credential-verification/vc-oid4vp.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Verifying Verifiable Credentials (W3C) via OID4VP
Video: https://youtu.be/NeC66H157T8
This guide shows you how to verify a W3C Verifiable Credential using the walt.id Verifier2 API and the [**OpenID for Verifiable Presentations (OID4VP)**](https://docs.walt.id/concepts/data-exchange-protocols/openid4vp.md) protocol. The verification process sends a credential request to a holder's wallet, receives the signed credential presentation, and validates it against your policies.
[**Verifiable Credential (VC)**](https://docs.walt.id/concepts/digital-credentials/verifiable-credentials-w3c.md): A digital equivalent of physical credentials such as a driver's license or university degree. VCs are cryptographically secure, privacy-respecting, and instantly verifiable.
[**OID4VP**](https://docs.walt.id/concepts/data-exchange-protocols/openid4vp.md): A protocol specifying how parties can present VCs in a way that's consistent and secure across platforms ensuring interoperability.
**Note:**
If you're migrating from the original Verifier (v1), see the [Migration Guide](https://docs.walt.id/community-stack/verifier2/migration-from-verifier1.md).
## Prerequisites
Before you begin, ensure you have:
- **Verifier2 API running** — Follow the [Getting Started](https://docs.walt.id/community-stack/verifier2/getting-started.md) or [Setup](https://docs.walt.id/community-stack/verifier2/setup.md) guide
- **A test wallet with credentials** — Use the [walt.id Wallet](https://docs.walt.id/community-stack/wallet/getting-started.md) for testing, or any OID4VP-compatible wallet
- **Publicly accessible URL** (for local development) — Use [ngrok](https://ngrok.com) or similar to expose `http://localhost:7004` if testing with a mobile wallet
**Note:**
**Local Development:** When running locally via Docker, the API is available at `http://localhost:7004` by default, whether standalone or via Docker Compose. Use this as your base URL in the examples below.
---
## Example 1: Basic Verification
This example requests a single W3C Verifiable Credential (OpenBadgeCredential) and verifies its cryptographic signature using the default signature policy.
**Option: CURL**
**Endpoint:** `POST /verification-session/create` | [API Reference](https://verifier2.portal.test.waltid.cloud/swagger/index.html#/Verification%20Session%20Management/post_verification_session_create)
##### Example Request
```bash
curl -X 'POST' \
'http://localhost:7004/verification-session/create' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"flow_type": "cross_device",
"core_flow": {
"dcql_query": {
"credentials": [
{
"id": "credential_1",
"format": "jwt_vc_json",
"meta": {
"type_values": [
[
"VerifiableCredential",
"OpenBadgeCredential"
]
]
}
}
]
},
"policies": {
"vc_policies": [
{
"policy": "signature"
}
]
}
}
}'
```
**Body Parameters**
- **flow_type**: _String_ (required) - Specifies how the credential request will be delivered to the holder's wallet. Options:
- `"cross_device"` — User scans a QR code on a different device (e.g. desktop browser → phone wallet)
- `"same_device"` — User clicks a deep link on the same device (e.g. phone browser → wallet app on same phone)
- `"dc_api_openid4vp"` — Browser/OS-native Digital Credentials API (Chrome, Safari, iOS/Android native) using OpenID4VP.
- `"dc_api_18013_7"` — Browser/OS-native Digital Credentials API (Chrome, Safari, iOS/Android native) using ISO 18013-7.
- **core_flow**: _Object_ (required) - Defines what credentials to request and which policies to apply. Contains:
- **dcql_query**: _Object_ (required) - Specifies the credential type, format, and claims to request using the [DCQL query language](https://identity.foundation/credential-query-language/). You can also use the DCQL builder [here](https://dcql.walt.id/).
- **credentials**: _Array_ - List of credential queries. Each credential query contains:
- **id**: _String_ - Unique identifier for this credential in the request (used in policy results)
- **format**: _String_ - Credential format. For W3C VCs, use the `"jwt_vc_json"` (JWT format).
- **meta.type_values**: _Array_ - List of acceptable credential type arrays. The credential must match at least one type array. Example: `[["VerifiableCredential", "OpenBadgeCredential"]]`.
- **claims**: _Array_ (optional) - Specific claims to request from the credential. Each claim has a `path` (JSON path to the claim). If omitted, all claims that are non-selectively disclosable are provided by the wallet.
- **policies**: _Object_ (optional) - Validation rules applied to the received credential. Defaults to signature verification only if omitted.
- **vc_policies**: _Array_ - Policies applied to individual credentials. Each policy has a `policy` field specifying the policy name. Available policies: `signature`, `expired`, `not-before`, `revoked-status-list`, `schema`, and more. See [Policies](https://docs.walt.id/community-stack/verifier2/policies/overview.md) for the complete list.
- **vp_policies**: _Array_ (optional) - Policies applied to the entire presentation (not shown in this example)
- **signedRequest**: _Boolean_ (optional) - Whether to sign the authorization request JWT. Required for HAIP profile compliance and DC API Annex C (ISO 18013-7) flows. Defaults to `false`. See [Signed Requests](#signed-requests) to learn more.
- **encryptedResponse**: _Boolean_ (optional) - Whether the wallet should encrypt the VP token response. Defaults to `false`. See [Encrypted Responses](#encrypted-responses) to learn more.
- **key**: _Object_ (optional) - Signing key for this verification session. Overrides the service-wide default `key` configured via the[`verifier-service.conf`](https://docs.walt.id/community-stack/verifier2/configurations/config-files/verifier-service.md#key-optional). Must be a serialized key in the format `{"type":"jwk","jwk":{...}}`. Required when `signedRequest: true` or `encryptedResponse: true` and no service-wide default is configured. See [Signed Requests](#signed-requests) and [Encrypted Responses](#encrypted-responses) for when this is needed.
- **x5c**: _Array of Strings_ (optional) - X.509 certificate chain for this verification session. Overrides the service-wide default `x5c` configured via [`verifier-service.conf`](https://docs.walt.id/community-stack/verifier2/configurations/config-files/verifier-service.md#x5c-optional). Must be an ordered chain in DER-encoded, Base64-encoded format where the first certificate corresponds to the `key`. Required for X.509-based client authentication (`x509_san_dns:` or `x509_hash:` as `clientId`) and signed Annex C requests. See [Signed Requests](#signed-requests) to learn more.
- **verifier_info**: _Object_ (optional) - An array of attestations about the Verifier relevant to the Credential Request. Each object is a JSON object with the following fields:
- **format**: _String_ - The format of the attestation. Currently only `"jwt"` is supported.
- **data**: _String_ - The attestation data in JWT format.
- **credentialIds**: _Array_ (optional) - Specifies which credentials in your DCQL query this attestation applies to. References the `id` fields from your `credentials` array in the DCQL query. If omitted, the attestation applies to all requested credentials.
- **url_config**: _Object_ (optional) - URL configuration for this session. Only applies to `cross_device` and `same_device` flows (not `dc_api`).
- **url_prefix**: _String_ (optional) - The publicly accessible base URL where the wallet will fetch the authorization request and post its response. Overrides the `urlPrefix` from `verifier-service.conf`.
- The service constructs: Request URI: `{urlPrefix}/{sessionId}/request` and Response URI: `{urlPrefix}/{sessionId}/response`
- **Must be reachable by the wallet** (use ngrok or public URL for local development)
- **url_host**: _String_ (optional) - The base URL used to construct the authorization request link. Overrides the `urlHost` from `verifier-service.conf`.
- For standard OID4VP flows, use `"openid4vp://authorize"` (default)
- For custom authorization endpoints (e.g. conformance testing), provide a full HTTPS URL
- **redirects**: _Object_ (optional) - Success and error redirect URLs (only for `cross_device` and `same_device` flows)
- **success_redirect_uri**: _String_ - URL to redirect the user after successful verification
- **error_redirect_uri**: _String_ - URL to redirect the user if verification fails
**Note:**
Many parameters have defaults configured in `verifier-service.conf`. Request-level parameters override these defaults for that specific session only. See [Configuration Defaults](#configuration-defaults) below.
---
##### Example Response
The response contains the session ID and authorization URLs to present to the user.
```json
{
"sessionId": "7c66c445-63e6-4a6a-8351-23ec78791b4e",
"bootstrapAuthorizationRequestUrl": "openid4vp://authorize?client_id=verifier2&request_uri=https%3A%2F%2Ff5dc-2a02-8388-17c1-5580-bddd-6845-3edf-388f.ngrok-free.app%2Fverification-session%2F7c66c445-63e6-4a6a-8351-23ec78791b4e%2Frequest",
"fullAuthorizationRequestUrl": "openid4vp://authorize?response_type=vp_token&client_id=verifier2&state=467f4e16-8c42-489a-a17c-cb6175063df3&response_mode=direct_post&nonce=ae4a922a-255f-4118-b9c6-35031c3546ff&response_uri=https%3A%2F%2Ff5dc-2a02-8388-17c1-5580-bddd-6845-3edf-388f.ngrok-free.app%2Fverification-session%2F7c66c445-63e6-4a6a-8351-23ec78791b4e%2Fresponse&dcql_query=%7B%22credentials%22%3A%5B%7B%22id%22%3A%22credential_1%22%2C%22format%22%3A%22jwt_vc_json%22%2C%22meta%22%3A%7B%22type_values%22%3A%5B%5B%22VerifiableCredential%22%2C%22OpenBadgeCredential%22%5D%5D%7D%7D%5D%7D&client_metadata=%7B%22vp_formats_supported%22%3A%7B%22mso_mdoc%22%3A%7B%22issuerauth_alg_values%22%3A%5B-7%2C-9%2C-50%5D%2C%22deviceauth_alg_values%22%3A%5B-7%2C-9%2C-50%2C-65537%5D%7D%7D%2C%22client_name%22%3A%22Verifier2%22%2C%22logo_uri%22%3A%22https%3A%2F%2Fimages.squarespace-cdn.com%2Fcontent%22%2C%22tos_uri%22%3A%22https%3A%2F%2Fexample.com%2Fterms%22%2C%22policy_uri%22%3A%22https%3A%2F%2Fexample.com%2Fprivacy%22%2C%22client_uri%22%3A%22https%3A%2F%2Fverifier.example.com%22%2C%22client_name%23fr-FR%22%3A%22V%C3%A9rificateur2%22%2C%22logo_uri%23fr-FR%22%3A%22https%3A%2F%2Fexample.com%2Flogo-fr.png%22%2C%22tos_uri%23fr-FR%22%3A%22https%3A%2F%2Fexample.com%2Fterms-fr%22%2C%22policy_uri%23fr-FR%22%3A%22https%3A%2F%2Fexample.com%2Fprivacy-fr%22%2C%22client_uri%23fr-FR%22%3A%22https%3A%2F%2Fverifier.example.com%2Ffr%22%7D"
}
```
**Response Fields:**
- **sessionId**: _String_ - The unique ID of this verification session. Use this to query the session status.
- **bootstrapAuthorizationRequestUrl**: _String_ - Shortened URL using `request_uri` by reference. **Use this for QR codes** to minimize QR code density.
- **fullAuthorizationRequestUrl**: _String_ - Complete URL with all parameters embedded. Use this if the wallet doesn't support `request_uri` (rare).
---
## Presenting the Request to the User
After creating the session, you need to present the authorization request to the user based on the flow type you chose.
### Cross-Device Flow (QR Code)
1. **Generate a QR code** from the `bootstrapAuthorizationRequestUrl`
2. **Display the QR code** to the user on your web page or application
3. **User scans the QR code** with their wallet app
4. **Wallet presents the credential** and posts the response to your verifier
5. **Monitor the session status** by polling the session endpoint or using SSE/callbacks (see below)
### Same-Device Flow (Deep Link)
1. **Redirect the user** to the `bootstrapAuthorizationRequestUrl`
2. **User's wallet app opens** automatically (via deep link)
3. **Wallet presents the credential** and posts the response to your verifier
4. **User is redirected** to your `successRedirectUri` or `errorRedirectUri`
5. **Query the session status** to retrieve the verification results
---
## Checking Verification Results
You can query the verification session to check if the user has presented their credential and whether validation succeeded.
**Option: CURL**
**Endpoint:** `GET /verification-session/{sessionId}/info` | [API Reference](https://verifier2.portal.test.waltid.cloud/swagger/index.html#/Verification%20Session%20Management/get_verification_session__verification_session__info)
##### Example Request
```bash
curl -X 'GET' \
'http://localhost:7004/verification-session/{sessionId}/info' \
-H 'accept: application/json'
```
**Path Parameters**
- **sessionId**: _String_ (required) - The unique ID of the verification session returned when you created the session. Use this to query the current status and results.
---
##### Example Response
The response contains the complete session information including setup, status, policy results, and presented credentials.
```json
{
"id": "ea6c2105-437c-40d4-af79-327922418b82",
"setup": {
"flow_type": "cross_device",
"core_flow": {
"dcql_query": {
"credentials": [
{
"id": "credential_1",
"format": "jwt_vc_json",
"multiple": false,
"meta": {
"type_values": [
["VerifiableCredential", "OpenBadgeCredential"]
],
"format": "jwt_vc_json"
},
"require_cryptographic_holder_binding": true
}
]
},
"signed_request": false,
"encrypted_response": false,
"policies": {
"vc_policies": [
{
"policy": "signature",
"id": "signature"
}
]
}
},
"url_config": {}
},
"creationDate": "2026-05-13T13:55:38.766985012Z",
"retentionDate": "2036-05-13T13:55:38.766985012Z",
"status": "SUCCESSFUL",
"attempted": true,
"reattemptable": true,
"bootstrapAuthorizationRequest": {
"client_id": "verifier2",
"request_uri": "https://f5dc-2a02-8388-17c1-5580-bddd-6845-3edf-388f.ngrok-free.app/verification-session/ea6c2105-437c-40d4-af79-327922418b82/request"
},
"bootstrapAuthorizationRequestUrl": "openid4vp://authorize?client_id=verifier2&request_uri=https%3A%2F%2Ff5dc-2a02-8388-17c1-5580-bddd-6845-3edf-388f.ngrok-free.app%2Fverification-session%2Fea6c2105-437c-40d4-af79-327922418b82%2Frequest",
"authorizationRequest": {
"response_type": "vp_token",
"client_id": "verifier2",
"state": "fd9d07ab-626e-4a99-8488-af39b1895ebf",
"response_mode": "direct_post",
"nonce": "f93b630f-6536-4b48-a20f-1edb2750a226",
"response_uri": "https://f5dc-2a02-8388-17c1-5580-bddd-6845-3edf-388f.ngrok-free.app/verification-session/ea6c2105-437c-40d4-af79-327922418b82/response",
"dcql_query": {
"credentials": [
{
"id": "credential_1",
"format": "jwt_vc_json",
"multiple": false,
"meta": {
"type_values": [
["VerifiableCredential", "OpenBadgeCredential"]
],
"format": "jwt_vc_json"
},
"require_cryptographic_holder_binding": true
}
]
},
"client_metadata": {
"vp_formats_supported": {
"jwt_vc_json": {},
"dc+sd-jwt": {},
"mso_mdoc": {
"issuerauth_alg_values": [-7, -9, -50],
"deviceauth_alg_values": [-7, -8, -9, -50]
}
}
}
},
"authorizationRequestUrl": "openid4vp://authorize?response_type=vp_token&client_id=verifier2&state=fd9d07ab-626e-4a99-8488-af39b1895ebf&response_mode=direct_post&nonce=f93b630f-6536-4b48-a20f-1edb2750a226&response_uri=https%3A%2F%2Ff5dc-2a02-8388-17c1-5580-bddd-6845-3edf-388f.ngrok-free.app%2Fverification-session%2Fea6c2105-437c-40d4-af79-327922418b82%2Fresponse&dcql_query=%7B%22credentials%22%3A%5B%7B%22id%22%3A%22credential_1%22%2C%22format%22%3A%22jwt_vc_json%22%2C%22meta%22%3A%7B%22type_values%22%3A%5B%5B%22VerifiableCredential%22%2C%22OpenBadgeCredential%22%5D%5D%7D%7D%5D%7D&client_metadata=%7B%22vp_formats_supported%22%3A%7B%22jwt_vc_json%22%3A%7B%7D%2C%22dc%2Bsd-jwt%22%3A%7B%7D%2C%22mso_mdoc%22%3A%7B%22issuerauth_alg_values%22%3A%5B-7%2C-9%2C-50%5D%2C%22deviceauth_alg_values%22%3A%5B-7%2C-8%2C-9%2C-50%5D%7D%7D%7D",
"requestMode": "REQUEST_URI",
"policies": {
"vp_policies": {
"jwt_vc_json": [
"jwt_vc_json/audience-check",
"jwt_vc_json/nonce-check",
"jwt_vc_json/envelope_signature",
"jwt_vc_json/exp-check",
"jwt_vc_json/nbf-check"
],
"dc+sd-jwt": [
"dc+sd-jwt/audience-check",
"dc+sd-jwt/kb-jwt_signature",
"dc+sd-jwt/nonce-check",
"dc+sd-jwt/sd_hash-check",
"dc+sd-jwt/kb-jwt_iat-check",
"dc+sd-jwt/exp-check",
"dc+sd-jwt/nbf-check",
"dc+sd-jwt/transaction-data-hashes-check",
"dc+sd-jwt/transaction-data-hash-check"
],
"mso_mdoc": [
"mso_mdoc/device-auth",
"mso_mdoc/device_key_auth",
"mso_mdoc/transaction-data-hash-check",
"mso_mdoc/issuer_auth",
"mso_mdoc/issuer_signed_integrity",
"mso_mdoc/mso"
]
},
"vc_policies": [
{
"policy": "signature",
"id": "signature"
}
]
},
"policy_results": {
"vp_policies": {
"credential_1": {
"jwt_vc_json/audience-check": {
"policy_executed": {
"policy": "jwt_vc_json/audience-check",
"id": "jwt_vc_json/audience-check",
"description": "Check if presentation audience matches expected audience for session"
},
"success": true,
"results": {
"presentation_audience": "verifier2",
"expected_audience": "verifier2"
},
"errors": [],
"execution_time": "PT0.000672375S"
},
"jwt_vc_json/nonce-check": {
"policy_executed": {
"policy": "jwt_vc_json/nonce-check",
"id": "jwt_vc_json/nonce-check",
"description": "Check if presentation nonce matches expected nonce for session"
},
"success": true,
"results": {},
"errors": [],
"execution_time": "PT0.000609417S"
},
"jwt_vc_json/envelope_signature": {
"policy_executed": {
"policy": "jwt_vc_json/envelope_signature",
"id": "jwt_vc_json/envelope_signature",
"description": "Verify signature using holders public key"
},
"success": true,
"results": {
"issuer": "did:key:z6MkqwuQv7kLB4uBm5SxNnh5XtXBi9EZMgua1rHXwB9apsFT",
"holder_key_jwk": {
"kty": "OKP",
"crv": "Ed25519",
"x": "qscSE66IVjjB-PO_2PWvEIlC24suW3yDOKuQ8ataJTY"
},
"verified_vp_jwt_content": {
"sub": "did:key:z6MkqwuQv7kLB4uBm5SxNnh5XtXBi9EZMgua1rHXwB9apsFT",
"nbf": 1785229156,
"iat": 1785229216,
"jti": "urn:uuid:ae08dc0e-7df8-40ba-9adf-4824636d3c23",
"iss": "did:key:z6MkqwuQv7kLB4uBm5SxNnh5XtXBi9EZMgua1rHXwB9apsFT",
"nonce": "f93b630f-6536-4b48-a20f-1edb2750a226",
"aud": "verifier2",
"vp": {
"@context": ["https://www.w3.org/2018/credentials/v1"],
"type": ["VerifiablePresentation"],
"id": "urn:uuid:ae08dc0e-7df8-40ba-9adf-4824636d3c23",
"holder": "did:key:z6MkqwuQv7kLB4uBm5SxNnh5XtXBi9EZMgua1rHXwB9apsFT",
"cnf": null,
"verifiableCredential": [
"eyJraWQiOiJkaWQ6andrOmV5SnJkSGtpT2lKRlF5..."
]
}
}
},
"errors": [],
"execution_time": "PT0.054552042S"
},
"jwt_vc_json/exp-check": {
"policy_executed": {
"policy": "jwt_vc_json/exp-check",
"clockSkewSeconds": 2,
"id": "jwt_vc_json/exp-check",
"description": "Check the VP JWT 'exp' claim — reject presentations that have expired"
},
"success": true,
"results": {},
"errors": [],
"execution_time": "PT0.000352042S"
},
"jwt_vc_json/nbf-check": {
"policy_executed": {
"policy": "jwt_vc_json/nbf-check",
"clockSkewSeconds": 2,
"id": "jwt_vc_json/nbf-check",
"description": "Check the VP JWT 'nbf' claim — reject presentations that are not yet valid"
},
"success": true,
"results": {
"nbf": "1785229156",
"now_epoch_seconds": "1785229216",
"clock_skew_seconds": "2"
},
"errors": [],
"execution_time": "PT0.001222292S"
}
}
},
"vc_policies": [
{
"policy": {
"policy": "signature",
"id": "signature"
},
"success": true,
"result": {
"verification_result": true,
"signed_credential": "eyJraWQiOiJkaWQ6andrOmV5SnJkSGtpT2lKRlF5...",
"credential_signature": {
"type": "signature-jwt",
"signature": "7-fHpTVOfOLKZfeBBwduFnGBE2i9D7eZOZ9TpdYfAerB1DbmEuqGEF06Xi3Ruat2TmW7wgojmnXDNLPxfO586A",
"jwtHeader": {
"kid": "did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IkcwUklOQmlGLW9RVUQzZDVER25lZ1F1WGVuSTI5SkRhTUdvTXZpb0tSQk0iLCJ5IjoiZWQzZUZHczJwRXRycDd2QVo3QkxjYnJVdHBLa1lXQVQySlBVUUs0bE40RSJ9#9vuaJyUxRx4KmHyoZ9kjJxMs_mjpnnf-mPM9nPMG51A",
"typ": "vc+jwt",
"alg": "ES256"
}
},
"verified_data": {
"iss": "did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IkcwUklOQmlGLW9RVUQzZDVER25lZ1F1WGVuSTI5SkRhTUdvTXZpb0tSQk0iLCJ5IjoiZWQzZUZHczJwRXRycDd2QVo3QkxjYnJVdHBLa1lXQVQySlBVUUs0bE40RSJ9",
"sub": "did:key:z6MkqwuQv7kLB4uBm5SxNnh5XtXBi9EZMgua1rHXwB9apsFT",
"credentialSubject": {
"achievement": {
"description": "This badge recognizes the development of the capacity to collaborate within a group environment.",
"name": "Teamwork",
"type": ["Achievement"],
"criteria": {
"narrative": "Team members are nominated for this badge by their peers and recognized upon review by Example Corp management."
},
"id": "https://example.com/achievements/21st-century-skills/teamwork"
},
"type": ["AchievementSubject"],
"id": "did:key:z6MkqwuQv7kLB4uBm5SxNnh5XtXBi9EZMgua1rHXwB9apsFT"
},
"type": ["VerifiableCredential", "OpenBadgeCredential"],
"credentialSchema": [
{
"type": "1EdTechJsonSchemaValidator2019",
"id": "https://purl.imsglobal.org/spec/ob/v3p0/schema/json/ob_v3p0_achievementcredential_schema.json"
}
],
"issuer": {
"name": "Jobs for the Future (JFF)",
"type": ["Profile"],
"image": "https://w3c-ccg.github.io/vc-ed/plugfest-1-2022/images/JFF_LogoLockup.png",
"url": "https://www.jff.org/",
"id": "did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IkcwUklOQmlGLW9RVUQzZDVER25lZ1F1WGVuSTI5SkRhTUdvTXZpb0tSQk0iLCJ5IjoiZWQzZUZHczJwRXRycDd2QVo3QkxjYnJVdHBLa1lXQVQySlBVUUs0bE40RSJ9"
},
"@context": [
"https://www.w3.org/ns/credentials/v2",
"https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json",
"https://purl.imsglobal.org/spec/ob/v3p0/extensions.json"
],
"name": "Example University Degree",
"id": "urn:uuid:f45e3ab1-5644-4a1f-ae9c-c5003c1cfe9f",
"validFrom": "2026-07-28T08:56:32.690218126Z",
"jti": "urn:uuid:f45e3ab1-5644-4a1f-ae9c-c5003c1cfe9f",
"iat": 1785228992,
"nbf": 1785228992
},
"successful_issuer_public_key": {
"kty": "EC",
"crv": "P-256",
"x": "G0RINBiF-oQUD3d5DGnegQuXenI29JDaMGoMvioKRBM",
"y": "ed3eFGs2pEtrp7vAZ7BLcbrUtpKkYWAT2JPUQK4lN4E"
},
"successful_issuer_public_key_id": "9vuaJyUxRx4KmHyoZ9kjJxMs_mjpnnf-mPM9nPMG51A"
},
"query_id": "credential_1",
"credential_index": 0
}
],
"specific_vc_policies": {},
"overallSuccess": true
},
"presented_raw_data": {
"vpToken": {
"credential_1": ["eyJraWQiOiJkaWQ6a2V5Ono2TWtxd3VRdjdrTEI0..."]
},
"state": "fd9d07ab-626e-4a99-8488-af39b1895ebf"
},
"presented_presentations": {
"credential_1": {
"type": "jwt_vc_json",
"format": "jwt_vc_json",
"jwt": "eyJraWQiOiJkaWQ6a2V5Ono2TWtxd3VRdjdrTEI0...",
"issuer": "did:key:z6MkqwuQv7kLB4uBm5SxNnh5XtXBi9EZMgua1rHXwB9apsFT",
"audience": "verifier2",
"nonce": "f93b630f-6536-4b48-a20f-1edb2750a226",
"vp": {
"@context": ["https://www.w3.org/2018/credentials/v1"],
"type": ["VerifiablePresentation"],
"id": "urn:uuid:ae08dc0e-7df8-40ba-9adf-4824636d3c23",
"holder": "did:key:z6MkqwuQv7kLB4uBm5SxNnh5XtXBi9EZMgua1rHXwB9apsFT",
"cnf": null,
"verifiableCredential": ["eyJraWQiOiJkaWQ6andrOmV5SnJkSGtpT2lKRlF5..."]
},
"credentials": [
{
"type": "vc-w3c_2",
"disclosables": {},
"credentialData": {
"iss": "did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IkcwUklOQmlGLW9RVUQzZDVER25lZ1F1WGVuSTI5SkRhTUdvTXZpb0tSQk0iLCJ5IjoiZWQzZUZHczJwRXRycDd2QVo3QkxjYnJVdHBLa1lXQVQySlBVUUs0bE40RSJ9",
"sub": "did:key:z6MkqwuQv7kLB4uBm5SxNnh5XtXBi9EZMgua1rHXwB9apsFT",
"credentialSubject": {
"achievement": {
"description": "This badge recognizes the development of the capacity to collaborate within a group environment.",
"name": "Teamwork",
"type": ["Achievement"],
"criteria": {
"narrative": "Team members are nominated for this badge by their peers and recognized upon review by Example Corp management."
},
"id": "https://example.com/achievements/21st-century-skills/teamwork"
},
"type": ["AchievementSubject"],
"id": "did:key:z6MkqwuQv7kLB4uBm5SxNnh5XtXBi9EZMgua1rHXwB9apsFT"
},
"type": ["VerifiableCredential", "OpenBadgeCredential"],
"credentialSchema": [
{
"type": "1EdTechJsonSchemaValidator2019",
"id": "https://purl.imsglobal.org/spec/ob/v3p0/schema/json/ob_v3p0_achievementcredential_schema.json"
}
],
"issuer": {
"name": "Jobs for the Future (JFF)",
"type": ["Profile"],
"image": "https://w3c-ccg.github.io/vc-ed/plugfest-1-2022/images/JFF_LogoLockup.png",
"url": "https://www.jff.org/",
"id": "did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IkcwUklOQmlGLW9RVUQzZDVER25lZ1F1WGVuSTI5SkRhTUdvTXZpb0tSQk0iLCJ5IjoiZWQzZUZHczJwRXRycDd2QVo3QkxjYnJVdHBLa1lXQVQySlBVUUs0bE40RSJ9"
},
"@context": [
"https://www.w3.org/ns/credentials/v2",
"https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json",
"https://purl.imsglobal.org/spec/ob/v3p0/extensions.json"
],
"name": "Example University Degree",
"id": "urn:uuid:f45e3ab1-5644-4a1f-ae9c-c5003c1cfe9f",
"validFrom": "2026-07-28T08:56:32.690218126Z",
"jti": "urn:uuid:f45e3ab1-5644-4a1f-ae9c-c5003c1cfe9f",
"iat": 1785228992,
"nbf": 1785228992
},
"issuer": "did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IkcwUklOQmlGLW9RVUQzZDVER25lZ1F1WGVuSTI5SkRhTUdvTXZpb0tSQk0iLCJ5IjoiZWQzZUZHczJwRXRycDd2QVo3QkxjYnJVdHBLa1lXQVQySlBVUUs0bE40RSJ9",
"subject": "did:key:z6MkqwuQv7kLB4uBm5SxNnh5XtXBi9EZMgua1rHXwB9apsFT",
"signature": {
"type": "signature-jwt",
"signature": "7-fHpTVOfOLKZfeBBwduFnGBE2i9D7eZOZ9TpdYfAerB1DbmEuqGEF06Xi3Ruat2TmW7wgojmnXDNLPxfO586A",
"jwtHeader": {
"kid": "did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IkcwUklOQmlGLW9RVUQzZDVER25lZ1F1WGVuSTI5SkRhTUdvTXZpb0tSQk0iLCJ5IjoiZWQzZUZHczJwRXRycDd2QVo3QkxjYnJVdHBLa1lXQVQySlBVUUs0bE40RSJ9#9vuaJyUxRx4KmHyoZ9kjJxMs_mjpnnf-mPM9nPMG51A",
"typ": "vc+jwt",
"alg": "ES256"
}
},
"signed": "eyJraWQiOiJkaWQ6andrOmV5SnJkSGtpT2lKRlF5...",
"format": "jwt_vc_json"
}
]
}
},
"presented_credentials": {
"credential_1": [
{
"type": "vc-w3c_2",
"disclosables": {},
"credentialData": {
"iss": "did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IkcwUklOQmlGLW9RVUQzZDVER25lZ1F1WGVuSTI5SkRhTUdvTXZpb0tSQk0iLCJ5IjoiZWQzZUZHczJwRXRycDd2QVo3QkxjYnJVdHBLa1lXQVQySlBVUUs0bE40RSJ9",
"sub": "did:key:z6MkqwuQv7kLB4uBm5SxNnh5XtXBi9EZMgua1rHXwB9apsFT",
"credentialSubject": {
"achievement": {
"description": "This badge recognizes the development of the capacity to collaborate within a group environment.",
"name": "Teamwork",
"type": ["Achievement"],
"criteria": {
"narrative": "Team members are nominated for this badge by their peers and recognized upon review by Example Corp management."
},
"id": "https://example.com/achievements/21st-century-skills/teamwork"
},
"type": ["AchievementSubject"],
"id": "did:key:z6MkqwuQv7kLB4uBm5SxNnh5XtXBi9EZMgua1rHXwB9apsFT"
},
"type": ["VerifiableCredential", "OpenBadgeCredential"],
"credentialSchema": [
{
"type": "1EdTechJsonSchemaValidator2019",
"id": "https://purl.imsglobal.org/spec/ob/v3p0/schema/json/ob_v3p0_achievementcredential_schema.json"
}
],
"issuer": {
"name": "Jobs for the Future (JFF)",
"type": ["Profile"],
"image": "https://w3c-ccg.github.io/vc-ed/plugfest-1-2022/images/JFF_LogoLockup.png",
"url": "https://www.jff.org/",
"id": "did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IkcwUklOQmlGLW9RVUQzZDVER25lZ1F1WGVuSTI5SkRhTUdvTXZpb0tSQk0iLCJ5IjoiZWQzZUZHczJwRXRycDd2QVo3QkxjYnJVdHBLa1lXQVQySlBVUUs0bE40RSJ9"
},
"@context": [
"https://www.w3.org/ns/credentials/v2",
"https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json",
"https://purl.imsglobal.org/spec/ob/v3p0/extensions.json"
],
"name": "Example University Degree",
"id": "urn:uuid:f45e3ab1-5644-4a1f-ae9c-c5003c1cfe9f",
"validFrom": "2026-07-28T08:56:32.690218126Z",
"jti": "urn:uuid:f45e3ab1-5644-4a1f-ae9c-c5003c1cfe9f",
"iat": 1785228992,
"nbf": 1785228992
},
"issuer": "did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IkcwUklOQmlGLW9RVUQzZDVER25lZ1F1WGVuSTI5SkRhTUdvTXZpb0tSQk0iLCJ5IjoiZWQzZUZHczJwRXRycDd2QVo3QkxjYnJVdHBLa1lXQVQySlBVUUs0bE40RSJ9",
"subject": "did:key:z6MkqwuQv7kLB4uBm5SxNnh5XtXBi9EZMgua1rHXwB9apsFT",
"signature": {
"type": "signature-jwt",
"signature": "7-fHpTVOfOLKZfeBBwduFnGBE2i9D7eZOZ9TpdYfAerB1DbmEuqGEF06Xi3Ruat2TmW7wgojmnXDNLPxfO586A",
"jwtHeader": {
"kid": "did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IkcwUklOQmlGLW9RVUQzZDVER25lZ1F1WGVuSTI5SkRhTUdvTXZpb0tSQk0iLCJ5IjoiZWQzZUZHczJwRXRycDd2QVo3QkxjYnJVdHBLa1lXQVQySlBVUUs0bE40RSJ9#9vuaJyUxRx4KmHyoZ9kjJxMs_mjpnnf-mPM9nPMG51A",
"typ": "vc+jwt",
"alg": "ES256"
}
},
"signed": "eyJraWQiOiJkaWQ6andrOmV5SnJkSGtpT2lKRlF5...",
"format": "jwt_vc_json"
}
]
}
}
```
**Note:**
Two fields are trimmed above for readability, not because they're absent:
- `presentation_validation_results` mirrors `policy_results.vp_policies` exactly — same per-credential policy results, kept as a separate top-level field for callers that only need the presentation-level checks.
- `presented_presentations.{credential_id}` also includes a `payload` object with the VP JWT's raw decoded claims (`sub`, `nbf`, `iat`, `jti`, `iss`, `nonce`, `aud`, `vp`) — the same information already surfaced via `issuer`, `audience`, `nonce`, and `vp` at the top level of that entry.
**Key Response Fields:**
- **id**: _String_ - The verification session ID
- **setup**: _Object_ - The complete session configuration that was used to create this verification session (the `VerificationSessionSetup` from the create request)
- **creationDate**: _String_ - ISO 8601 timestamp when the session was created
- **retentionDate**: _String_ - ISO 8601 timestamp
- **status**: _String_ - The current status of the verification session. Possible values:
- `"UNKNOWN"` — Session ended up in unknown flow (should be avoided)
- `"ACTIVE"` — Session was created and is active (can be used)
- `"UNUSED"` — Session was not used yet, but not expired (can be used)
- `"IN_USE"` — AuthorizationRequest was requested by wallet
- `"VALIDATING_RECEIVED_REQUEST"` — Checking if received presentation will be processed
- `"PROCESSING_FLOW"` — Received presentation is being processed (validation + policy execution)
- `"EXPIRED"` — Verification request expired without being utilized
- `"SUCCESSFUL"` — Verification completed fully successfully (all validation & policies passed)
- `"FAILED"` — Verification was unsuccessful (presentation validation or policies failed)
- **attempted**: _Boolean_ - Whether the wallet has attempted to respond to the verification request
- **reattemptable**: _Boolean_ - Whether the user can retry this verification session if it failed
- **policies**: _Object_ - The policies configured for this session:
- **vp_policies**: _Object_ (optional) - Policies to run on presentations
- **vc_policies**: _Array_ (optional) - Policies to run on credentials
- **specific_vc_policies**: _Object_ (optional) - Policies to run on specific credential IDs
- **policy_results**: _Object_ (optional) - Detailed results for all policies applied to the verification (only present after presentation is received):
- **vp_policies**: _Object_ - Results for presentation-level policies (audience check, nonce check, envelope signature, expiry, not-before), organized by credential ID. Structure: `{ "credential_id": { "policy_name": { ... } } }`
- **vc_policies**: _Array_ - Results for credential-level policies (signature, expiry, schema, etc.). Each item contains `policy`, `success`, and `result`.
- **specific_vc_policies**: _Object_ - Results for credential-specific policies
- **overallSuccess**: _Boolean_ (computed) - Whether all policies passed
- **presentation_validation_results**: _Object_ (optional) - Presentation-level policy results, keyed by credential ID; the same content as `policy_results.vp_policies`
- **presented_raw_data**: _Object_ (optional) - The raw wire-level data the wallet submitted: `vpToken` (the VP JWT(s) per credential ID, unparsed) and the `state` value echoed back
- **presented_presentations**: _Object_ (optional) - The wallet's presentation(s), keyed by credential ID, with the VP JWT decoded into `payload`, plus parsed `credentials`
- **redirects**: _Object_ (optional) - Success and error redirect URIs if configured
**Note:**
Instead of polling the session status, you can use **callbacks** or **Server Sent Events (SSE)** to be notified when the verification completes. See [Callbacks & SSE](https://docs.walt.id/community-stack/verifier2/callback-and-sse.md) for details.
---
## Example 2: Requesting Multiple Credentials
You can request multiple credentials in a single verification session. The wallet will present all matching credentials, and policies will be applied to each one.
This example requests both an OpenBadgeCredential and a VerifiableId in JWT format.
**Option: CURL**
**Endpoint:** `POST /verification-session/create` | [API Reference](https://verifier2.portal.test.waltid.cloud/swagger/index.html#/Verification%20Session%20Management/post_verification_session_create)
##### Example Request
```bash
curl -X 'POST' \
'http://localhost:7004/verification-session/create' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"flow_type": "cross_device",
"core_flow": {
"dcql_query": {
"credentials": [
{
"id": "example_openbadge_jwt_vc",
"format": "jwt_vc_json",
"meta": {
"type_values": [
[
"VerifiableCredential",
"OpenBadgeCredential"
]
]
},
"claims": [
{
"path": [
"name"
]
}
]
},
{
"id": "example_verifiableid_jwt_vc",
"format": "jwt_vc_json",
"meta": {
"type_values": [
[
"VerifiableCredential",
"VerifiableAttestation",
"VerifiableId"
]
]
},
"claims": [
{
"path": [
"issuanceDate"
]
}
]
}
]
},
"policies": {
"vc_policies": [
{
"policy": "signature"
}
]
}
}
}'
```
The wallet will present both credentials (if available), and the signature policy will be applied to each one. The `policyResults` in the session info response will contain separate results for each credential using their `id` values.
---
## Understanding Flow Types and Core Concepts
Now that you've seen verification in action, let's understand the underlying structure.
### Verification Session Structure
A `VerificationSession` defines the full lifecycle: authorization request → user presentation → policy validation → final result.
Every verification request follows this structure:
```json
{
"flow_type": "cross_device | same_device | dc_api",
"core_flow": { /* common to all flows */ },
"url_config": { /* specific to cross_device and same_device */ },
"redirects": { /* optional */ }
}
```
**Why this structure?**
- **flow_type** determines how the credential request is delivered (QR code vs deep link vs browser API)
- **core_flow** contains the credential query and policies (same regardless of delivery method)
- **url_config** and **redirects** are flow-specific attributes
This separation makes it easy to switch between flows without changing your credential requirements.
### Flow Type Comparison
| Flow Type | Use Case | Required Attributes | User Experience |
|-----------|----------|---------------------|-----------------|
| `cross_device` | User scans QR code from desktop | None beyond core flow | Desktop shows QR → User scans with phone → Phone wallet opens |
| `same_device` | User clicks link on mobile | `redirects` (success/error URLs) | Browser redirects to wallet → Wallet opens → User returns to browser |
| `dc_api` | Browser-native credential exchange | `expectedOrigins` (array of allowed origins) | Browser shows native credential picker → User selects → Instant return |
---
## Configuration Defaults
Many session parameters can be configured globally in `verifier-service.conf` and overridden per-request when needed.
| Parameter | Config File Location | Request Override Path | When to Override |
|-----------|---------------------|----------------------|------------------|
| `urlPrefix` | `verifier-service.conf` → `urlPrefix` | `url_config.url_prefix` | Testing with ngrok, different environments per session |
| `urlHost` | `verifier-service.conf` → `urlHost` | `url_config.url_host` | Custom authorization endpoints, conformance testing |
| `clientId` | `verifier-service.conf` → `clientId` | `core_flow.clientId` | Different verifier identities per use case |
| `clientMetadata` | `verifier-service.conf` → `clientMetadata` | `core_flow.client_metadata` | Session-specific branding, multi-tenant scenarios |
| Signing key | `verifier-service.conf` → `key` | `core_flow.key` | Different signing keys per session (advanced) |
| Certificate chain | `verifier-service.conf` → `x5c` | `core_flow.x5c` | Different certificate chains per session (x509 flows) |
**Note:**
**Best Practice:** Set defaults in `verifier-service.conf` for your primary use case, and override per-request only when necessary. This keeps your API calls clean and reduces duplication.
See [Verifier Service Configuration](https://docs.walt.id/community-stack/verifier2/configurations/config-files/verifier-service.md) for detailed configuration documentation.
---
## Advanced Options
### Signed Requests
Sign the authorization request JWT using a verifier signing key. This is required for:
- **HAIP profile** compliance
- **DC API Annex C** (ISO 18013-7) flows
Enable by setting `signedRequest: true` in the request body.
**Key Configuration Required:** Signed requests require a signing key (`key`) to be configured. You can either:
- Set the `key` parameter in the [`verifier-service.conf`](https://docs.walt.id/community-stack/verifier2/configurations/config-files/verifier-service.md#key-optional) as a service-wide default
- Override or provide the `key` directly in the verification request body
**Certificate Chain (`x5c`) Required for:**
- X.509-based client authentication when using `x509_san_dns:` or `x509_hash:` as the `clientId` format
- Signed Annex C (ISO 18013-7) requests
The `x5c` parameter can be configured the same way as `key` — as a service-wide default or per-session override.
### Encrypted Responses
The wallet can encrypt the VP token response. Enable by setting `encryptedResponse: true` in the request body.
**Key Configuration Required:** Encrypted responses require a key (`key`) for decrypting the VP token response. Configure it via [`verifier-service.conf`](https://docs.walt.id/community-stack/verifier2/configurations/config-files/verifier-service.md#key-optional) or provide it in the verification request body.
### Custom Policies
Beyond signature verification, you can apply additional policies:
```json
"policies": {
"vc_policies": [
{ "policy": "signature" },
{ "policy": "expired" },
{ "policy": "not-before" },
{ "policy": "schema" }
]
}
```
See [Policies](https://docs.walt.id/community-stack/verifier2/policies/overview.md) for the complete list and configuration options.
---
## Next Steps
- **Apply custom policies** — See [Policies](https://docs.walt.id/community-stack/verifier2/policies/overview.md) for validation rules beyond signature verification
- **Set up notifications** — Use [Callbacks & SSE](https://docs.walt.id/community-stack/verifier2/callback-and-sse.md) instead of polling for session status
- **Verify other credential types** — Try [SD-JWT VC](https://docs.walt.id/community-stack/verifier2/credential-verification/sd-jwt-vc-oid4vp.md) or [mDL](https://docs.walt.id/community-stack/verifier2/credential-verification/mdl-oid4vp.md)
- **Use DC API** — Try the [Digital Credentials API](https://docs.walt.id/community-stack/verifier2/credential-verification/dc-api.md) for browser-native credential exchange
--------------------------------------------------------------------------------
PAGE: Verifying SD-JWT VC Credentials via OID4VP
URL: https://docs.walt.id/community-stack/verifier2/credential-verification/sd-jwt-vc-oid4vp
RAW: https://docs.walt.id/community-stack/verifier2/credential-verification/sd-jwt-vc-oid4vp.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Verifying SD-JWT VC credentials via OID4VP
Video: https://youtu.be/7jTb4DAWHMo
This guide shows you how to verify [**SD-JWT VC**](https://docs.walt.id/concepts/digital-credentials/sd-jwt-vc.md) credentials using the Verifier2 API via [**OpenID for Verifiable Presentations (OID4VP)**](https://docs.walt.id/concepts/data-exchange-protocols/openid4vp.md)
[**SD-JWT VCs**](https://docs.walt.id/concepts/digital-credentials/sd-jwt-vc.md): Credentials based on the [IETF SD-JWT VC standard](https://datatracker.ietf.org/doc/draft-ietf-oauth-sd-jwt-vc/) which support selective disclosure, allowing users to share only specific claims from their credentials.
[**OID4VP**](https://docs.walt.id/concepts/data-exchange-protocols/openid4vp.md): A protocol specifying how parties can present VCs in a way that's consistent and secure across platforms ensuring interoperability.
**Note:**
If you're migrating from the original Verifier (v1), see the [Migration Guide](https://docs.walt.id/community-stack/verifier2/migration-from-verifier1.md).
## Prerequisites
Before you begin, ensure you have:
- **Verifier2 API running** — Follow the [Getting Started](https://docs.walt.id/community-stack/verifier2/getting-started.md) or [Setup](https://docs.walt.id/community-stack/verifier2/setup.md) guide
- **A test wallet with credentials** — Use the [walt.id Wallet](https://docs.walt.id/community-stack/wallet/getting-started.md) for testing, or any OID4VP-compatible wallet
- **Publicly accessible URL** (for local development) — Use [ngrok](https://ngrok.com) or similar to expose `http://localhost:7004` if testing with a mobile wallet
**Note:**
**Local Development:** When running locally via Docker, the API is available at `http://localhost:7004` by default, whether standalone or via Docker Compose. Use this as your base URL in the examples below.
---
## Example 1: Basic Verification
This example requests an SD-JWT identity credential and verifies its signature.
**Option: CURL**
**Endpoint:** `POST /verification-session/create` | [API Reference](https://verifier2.portal.test.waltid.cloud/swagger/index.html#/Verification%20Session%20Management/post_verification_session_create)
##### Example Request
```bash
curl -X 'POST' \
'http://localhost:7004/verification-session/create' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"flow_type": "cross_device",
"core_flow": {
"dcql_query": {
"credentials": [
{
"id": "credential_1",
"format": "dc+sd-jwt",
"meta": {
"vct_values": [
"https://issuer.demo.walt.id/draft13/identity_credential"
]
}
}
]
}
}
}'
```
**Body Parameters**
- **flow_type**: _String_ (required) - Specifies how the credential request will be delivered to the holder's wallet. Options:
- `"cross_device"` — User scans a QR code on a different device (e.g. desktop browser → phone wallet)
- `"same_device"` — User clicks a deep link on the same device (e.g. phone browser → wallet app on same phone)
- `"dc_api_openid4vp"` — Browser/OS-native Digital Credentials API (Chrome, Safari, iOS/Android native) using OpenID4VP.
- `"dc_api_18013_7"` — Browser/OS-native Digital Credentials API (Chrome, Safari, iOS/Android native) using ISO 18013-7.
- **core_flow**: _Object_ (required) - Defines what credentials to request and which policies to apply. Contains:
- **dcql_query**: _Object_ (required) - Specifies the credential type, format, and claims to request using the [DCQL query language](https://identity.foundation/credential-query-language/). You can also use the DCQL builder [here](https://dcql.walt.id/).
- **credentials**: _Array_ - List of credential queries. Each credential query contains:
- **id**: _String_ - Unique identifier for this credential in the request (used in policy results)
- **format**: _String_ - Credential format. For SD-JWT VCs, use the `"dc+sd-jwt"`.
- **meta.vct_values**: _Array_ - List of of credential types. Example: `["https://issuer.demo.walt.id/draft13/identity_credential"]`.
- **claims**: _Array_ (optional) - Specific claims to request from the credential. Each claim has a `path` (JSON path to the claim). If omitted, all claims that are non-selectively disclosable are provided by the wallet.
- **policies**: _Object_ (optional) - Validation rules applied to the received credential. Defaults to signature verification only if omitted.
- **vc_policies**: _Array_ - Policies applied to individual credentials. Each policy has a `policy` field specifying the policy name. Available policies: `signature`, `expired`, `not-before`, `revoked-status-list`, `schema`, and more. See [Policies](https://docs.walt.id/community-stack/verifier2/policies/overview.md) for the complete list.
- **vp_policies**: _Array_ (optional) - Policies applied to the entire presentation (not shown in this example)
- **signedRequest**: _Boolean_ (optional) - Whether to sign the authorization request JWT. Required for HAIP profile compliance and DC API Annex C (ISO 18013-7) flows. Defaults to `false`. See [Signed Requests](#signed-requests) to learn more.
- **encryptedResponse**: _Boolean_ (optional) - Whether the wallet should encrypt the VP token response. Defaults to `false`. See [Encrypted Responses](#encrypted-responses) to learn more.
- **key**: _Object_ (optional) - Signing key for this verification session. Overrides the service-wide default `key` configured via the[`verifier-service.conf`](https://docs.walt.id/community-stack/verifier2/configurations/config-files/verifier-service.md#key-optional). Must be a serialized key in the format `{"type":"jwk","jwk":{...}}`. Required when `signedRequest: true` or `encryptedResponse: true` and no service-wide default is configured. See [Signed Requests](#signed-requests) and [Encrypted Responses](#encrypted-responses) for when this is needed.
- **x5c**: _Array of Strings_ (optional) - X.509 certificate chain for this verification session. Overrides the service-wide default `x5c` configured via [`verifier-service.conf`](https://docs.walt.id/community-stack/verifier2/configurations/config-files/verifier-service.md#x5c-optional). Must be an ordered chain in DER-encoded, Base64-encoded format where the first certificate corresponds to the `key`. Required for X.509-based client authentication (`x509_san_dns:` or `x509_hash:` as `clientId`) and signed Annex C requests. See [Signed Requests](#signed-requests) to learn more.
- **verifier_info**: _Object_ (optional) - An array of attestations about the Verifier relevant to the Credential Request. Each object is a JSON object with the following fields:
- **format**: _String_ - The format of the attestation. Currently only `"jwt"` is supported.
- **data**: _String_ - The attestation data in JWT format.
- **credentialIds**: _Array_ (optional) - Specifies which credentials in your DCQL query this attestation applies to. References the `id` fields from your `credentials` array in the DCQL query. If omitted, the attestation applies to all requested credentials.
- **openid**: _Object_ (optional) - OpenID4VP-specific extensions
- **transactionData**: _Array_ (optional) - Bind the credential presentation to a specific transaction (e.g., payment authorization, account access). Each entry references credential IDs and includes transaction details. See [Transaction Data Authorization](#transaction-data-authorization) for full documentation.
- **url_config**: _Object_ (optional) - URL configuration for this session. Only applies to `cross_device` and `same_device` flows (not `dc_api`).
- **url_prefix**: _String_ (optional) - The publicly accessible base URL where the wallet will fetch the authorization request and post its response. Overrides the `urlPrefix` from `verifier-service.conf`.
- The service constructs: Request URI: `{urlPrefix}/{sessionId}/request` and Response URI: `{urlPrefix}/{sessionId}/response`
- **Must be reachable by the wallet** (use ngrok or public URL for local development)
- **url_host**: _String_ (optional) - The base URL used to construct the authorization request link. Overrides the `urlHost` from `verifier-service.conf`.
- For standard OID4VP flows, use `"openid4vp://authorize"` (default)
- For custom authorization endpoints (e.g. conformance testing), provide a full HTTPS URL
- **redirects**: _Object_ (optional) - Success and error redirect URLs (only for `cross_device` and `same_device` flows)
- **success_redirect_uri**: _String_ - URL to redirect the user after successful verification
- **error_redirect_uri**: _String_ - URL to redirect the user if verification fails
**Note:**
Many parameters have defaults configured in `verifier-service.conf`. Request-level parameters override these defaults for that specific session only. See [Configuration Defaults](#configuration-defaults) below.
---
##### Example Response
```json
{
"sessionId": "14d60fa2-4e7d-4a34-a291-80da0e053fab",
"bootstrapAuthorizationRequestUrl": "openid4vp://authorize?client_id=verifier2&request_uri=https%3A%2F%2Ff5dc-2a02-8388-17c1-5580-bddd-6845-3edf-388f.ngrok-free.app%2Fverification-session%2F14d60fa2-4e7d-4a34-a291-80da0e053fab%2Frequest",
"fullAuthorizationRequestUrl": "openid4vp://authorize?response_type=vp_token&client_id=verifier2&state=a697ef7d-fc66-4ccd-b79a-3aa1b93836df&response_mode=direct_post&nonce=4c44c117-2d26-4838-b42f-52b0e0faa070&response_uri=https%3A%2F%2Ff5dc-2a02-8388-17c1-5580-bddd-6845-3edf-388f.ngrok-free.app%2Fverification-session%2F14d60fa2-4e7d-4a34-a291-80da0e053fab%2Fresponse&dcql_query=%7B%22credentials%22%3A%5B%7B%22id%22%3A%22credential_1%22%2C%22format%22%3A%22dc%2Bsd-jwt%22%2C%22meta%22%3A%7B%22vct_values%22%3A%5B%22https%3A%2F%2Fissuer.demo.walt.id%2Fdraft13%2Fidentity_credential%22%5D%7D%7D%5D%7D&client_metadata=%7B%22vp_formats_supported%22%3A%7B%22mso_mdoc%22%3A%7B%22issuerauth_alg_values%22%3A%5B-7%2C-9%2C-50%5D%2C%22deviceauth_alg_values%22%3A%5B-7%2C-9%2C-50%2C-65537%5D%7D%7D%2C%22client_name%22%3A%22Verifier2%22%2C%22logo_uri%22%3A%22https%3A%2F%2Fimages.squarespace-cdn.com%2Fcontent%22%2C%22tos_uri%22%3A%22https%3A%2F%2Fexample.com%2Fterms%22%2C%22policy_uri%22%3A%22https%3A%2F%2Fexample.com%2Fprivacy%22%2C%22client_uri%22%3A%22https%3A%2F%2Fverifier.example.com%22%2C%22client_name%23fr-FR%22%3A%22V%C3%A9rificateur2%22%2C%22logo_uri%23fr-FR%22%3A%22https%3A%2F%2Fexample.com%2Flogo-fr.png%22%2C%22tos_uri%23fr-FR%22%3A%22https%3A%2F%2Fexample.com%2Fterms-fr%22%2C%22policy_uri%23fr-FR%22%3A%22https%3A%2F%2Fexample.com%2Fprivacy-fr%22%2C%22client_uri%23fr-FR%22%3A%22https%3A%2F%2Fverifier.example.com%2Ffr%22%7D"
}
```
**Response Fields:**
- **sessionId**: _String_ - The unique ID of this verification session. Use this to query the session status.
- **bootstrapAuthorizationRequestUrl**: _String_ - Shortened URL using `request_uri` by reference. **Use this for QR codes** to minimize QR code density.
- **fullAuthorizationRequestUrl**: _String_ - Complete URL with all parameters embedded. Use this if the wallet doesn't support `request_uri` (rare).
## Presenting the Request to the User
After creating the session, you need to present the authorization request to the user based on the flow type you chose.
### Cross-Device Flow (QR Code)
1. **Generate a QR code** from the `bootstrapAuthorizationRequestUrl`
2. **Display the QR code** to the user on your web page or application
3. **User scans the QR code** with their wallet app
4. **Wallet presents the credential** and posts the response to your verifier
5. **Monitor the session status** by polling the session endpoint or using SSE/callbacks (see below)
### Same-Device Flow (Deep Link)
1. **Redirect the user** to the `bootstrapAuthorizationRequestUrl`
2. **User's wallet app opens** automatically (via deep link)
3. **Wallet presents the credential** and posts the response to your verifier
4. **User is redirected** to your `successRedirectUri` or `errorRedirectUri`
5. **Query the session status** to retrieve the verification results
## Checking Verification Results
You can query the verification session to check if the user has presented their credential and whether validation succeeded.
**Option: CURL**
**Endpoint:** `GET /verification-session/{sessionId}/info` | [API Reference](https://verifier2.portal.test.waltid.cloud/swagger/index.html#/Verification%20Session%20Management/get_verification_session__sessionId__info)
##### Example Request
```bash
curl -X 'GET' \
'http://localhost:7004/verification-session/14d60fa2-4e7d-4a34-a291-80da0e053fab/info' \
-H 'accept: application/json'
```
**Path Parameters**
- **sessionId**: _String_ (required) - The unique ID of the verification session returned when you created the session. Use this to query the current status and results.
---
##### Example Response
```json
{
"id": "14d60fa2-4e7d-4a34-a291-80da0e053fab",
"setup": {
"flow_type": "cross_device",
"core_flow": {
"dcql_query": {
"credentials": [
{
"id": "credential_1",
"format": "dc+sd-jwt",
"multiple": false,
"meta": {
"vct_values": [
"https://issuer.demo.walt.id/draft13/identity_credential"
],
"format": "dc+sd-jwt"
},
"require_cryptographic_holder_binding": true
}
]
},
"signed_request": false,
"encrypted_response": false,
"policies": {}
},
"url_config": {}
},
"creationDate": "2026-05-13T14:14:52.688020004Z",
"expirationDate": "2026-05-13T14:19:52.688020004Z",
"retentionDate": "2036-05-13T14:14:52.688020004Z",
"status": "SUCCESSFUL",
"attempted": true,
"reattemptable": true,
"bootstrapAuthorizationRequest": {
"client_id": "verifier2",
"request_uri": "https://f5dc-2a02-8388-17c1-5580-bddd-6845-3edf-388f.ngrok-free.app/verification-session/14d60fa2-4e7d-4a34-a291-80da0e053fab/request"
},
"bootstrapAuthorizationRequestUrl": "openid4vp://authorize?client_id=verifier2&request_uri=https%3A%2F%2Ff5dc-2a02-8388-17c1-5580-bddd-6845-3edf-388f.ngrok-free.app%2Fverification-session%2F14d60fa2-4e7d-4a34-a291-80da0e053fab%2Frequest",
"authorizationRequest": {
"response_type": "vp_token",
"client_id": "verifier2",
"state": "a697ef7d-fc66-4ccd-b79a-3aa1b93836df",
"response_mode": "direct_post",
"nonce": "4c44c117-2d26-4838-b42f-52b0e0faa070",
"response_uri": "https://f5dc-2a02-8388-17c1-5580-bddd-6845-3edf-388f.ngrok-free.app/verification-session/14d60fa2-4e7d-4a34-a291-80da0e053fab/response",
"dcql_query": {
"credentials": [
{
"id": "credential_1",
"format": "dc+sd-jwt",
"multiple": false,
"meta": {
"vct_values": [
"https://issuer.demo.walt.id/draft13/identity_credential"
],
"format": "dc+sd-jwt"
},
"require_cryptographic_holder_binding": true
}
]
},
"client_metadata": {
"vp_formats_supported": {
"mso_mdoc": {
"issuerauth_alg_values": [-7, -9, -50],
"deviceauth_alg_values": [-7, -9, -50, -65537]
}
},
"client_name": "Verifier2",
"logo_uri": "https://images.squarespace-cdn.com/content",
"tos_uri": "https://example.com/terms",
"policy_uri": "https://example.com/privacy",
"client_uri": "https://verifier.example.com"
}
},
"authorizationRequestUrl": "openid4vp://authorize?response_type=vp_token&client_id=verifier2&state=a697ef7d-fc66-4ccd-b79a-3aa1b93836df&response_mode=direct_post&nonce=4c44c117-2d26-4838-b42f-52b0e0faa070&response_uri=https%3A%2F%2Ff5dc-2a02-8388-17c1-5580-bddd-6845-3edf-388f.ngrok-free.app%2Fverification-session%2F14d60fa2-4e7d-4a34-a291-80da0e053fab%2Fresponse&dcql_query=%7B%22credentials%22%3A%5B%7B%22id%22%3A%22credential_1%22%2C%22format%22%3A%22dc%2Bsd-jwt%22%2C%22meta%22%3A%7B%22vct_values%22%3A%5B%22https%3A%2F%2Fissuer.demo.walt.id%2Fdraft13%2Fidentity_credential%22%5D%7D%7D%5D%7D&client_metadata=%7B%22vp_formats_supported%22%3A%7B%22mso_mdoc%22%3A%7B%22issuerauth_alg_values%22%3A%5B-7%2C-9%2C-50%5D%2C%22deviceauth_alg_values%22%3A%5B-7%2C-9%2C-50%2C-65537%5D%7D%7D%2C%22client_name%22%3A%22Verifier2%22%2C%22logo_uri%22%3A%22https%3A%2F%2Fimages.squarespace-cdn.com%2Fcontent%22%2C%22tos_uri%22%3A%22https%3A%2F%2Fexample.com%2Fterms%22%2C%22policy_uri%22%3A%22https%3A%2F%2Fexample.com%2Fprivacy%22%2C%22client_uri%22%3A%22https%3A%2F%2Fverifier.example.com%22%7D",
"requestMode": "REQUEST_URI",
"policies": {
"vp_policies": {
"dc+sd-jwt": [
"dc+sd-jwt/audience-check",
"dc+sd-jwt/kb-jwt_signature",
"dc+sd-jwt/nonce-check",
"dc+sd-jwt/sd_hash-check"
]
},
"vc_policies": [
{
"policy": "signature",
"id": "signature"
}
]
},
"policy_results": {
"vp_policies": {
"credential_1": {
"dc+sd-jwt/audience-check": {
"policy_executed": {
"policy": "dc+sd-jwt/audience-check",
"id": "dc+sd-jwt/audience-check",
"description": "Check if presentation audience matches expected audience for session"
},
"success": true,
"results": {
"presentation_audience": "verifier2",
"expected_audience": "verifier2"
},
"errors": [],
"execution_time": "PT0.000992042S"
},
"dc+sd-jwt/kb-jwt_signature": {
"policy_executed": {
"policy": "dc+sd-jwt/kb-jwt_signature",
"id": "dc+sd-jwt/kb-jwt_signature",
"description": "Verify the KB-JWTs signature with the holders key"
},
"success": true,
"results": {
"holder_key_jwk": {
"kty": "OKP",
"crv": "Ed25519",
"kid": "v1sbluyFJrMT6Lt46S4N_CBH1g7OWHpm_f3uK93dSLE",
"x": "jxQwRV7tHc3W_JkKVsDZL7_DtP4zyNJtQ4Yr1z7-Md4"
}
},
"errors": [],
"execution_time": "PT0.002432834S"
},
"dc+sd-jwt/nonce-check": {
"policy_executed": {
"policy": "dc+sd-jwt/nonce-check",
"description": "Check if presentation nonce matches expected nonce for session"
},
"success": true,
"results": {},
"errors": [],
"execution_time": "PT0.001029042S"
},
"dc+sd-jwt/sd_hash-check": {
"policy_executed": {
"policy": "dc+sd-jwt/sd_hash-check",
"description": "Verify SD-JWT Key Binding by recalculating SD Hashes"
},
"success": true,
"results": {
"presentation_sdhash": "KQKyptFb7obYaGPDRQvNHEKCSispCoFYGmbe1WUR_wI",
"recalculated_sdhash": "KQKyptFb7obYaGPDRQvNHEKCSispCoFYGmbe1WUR_wI"
},
"errors": [],
"execution_time": "PT0.003427750S"
}
}
},
"vc_policies": [
{
"policy": {
"policy": "signature",
"id": "signature"
},
"success": true,
"result": {
"verification_result": true,
"verified_data": {
"given_name": "John",
"family_name": "Doe",
"email": "johndoe@example.com",
"phone_number": "+1-202-555-0101",
"address": {
"street_address": "123 Main St",
"locality": "Anytown",
"region": "Anystate",
"country": "US"
},
"is_over_18": true,
"is_over_21": true,
"is_over_65": true,
"vct": "https://issuer.demo.walt.id/draft13/identity_credential"
}
}
}
],
"overallSuccess": true
},
"presented_credentials": {
"credential_1": [
{
"type": "vc-sd_jwt",
"format": "dc+sd-jwt",
"credentialData": {
"given_name": "John",
"family_name": "Doe",
"email": "johndoe@example.com",
"phone_number": "+1-202-555-0101",
"address": {
"street_address": "123 Main St",
"locality": "Anytown",
"region": "Anystate",
"country": "US"
},
"is_over_18": true,
"is_over_21": true,
"is_over_65": true,
"vct": "https://issuer.demo.walt.id/draft13/identity_credential"
},
"issuer": "did:key:z6MkjoRhq1jSNJdLiruSXrFFxagqrztZaXHqHGUTKJbcNywp"
}
]
}
}
```
**Key Response Fields:**
- **id**: _String_ - The verification session ID
- **setup**: _Object_ - The complete session configuration that was used to create this verification session (the `VerificationSessionSetup` from the create request)
- **creationDate**: _String_ - ISO 8601 timestamp when the session was created
- **expirationDate**: _String_ - ISO 8601 timestamp when the session expires (default: 10 minutes after creation). Session will expire if left unused (no presentation pushed). Once a presentation is received, the session is no longer eligible for expiry.
- **status**: _String_ - The current status of the verification session. Possible values:
- `"UNKNOWN"` — Session ended up in unknown flow (should be avoided)
- `"ACTIVE"` — Session was created and is active (can be used)
- `"UNUSED"` — Session was not used yet, but not expired (can be used)
- `"IN_USE"` — AuthorizationRequest was requested by wallet
- `"VALIDATING_RECEIVED_REQUEST"` — Checking if received presentation will be processed
- `"PROCESSING_FLOW"` — Received presentation is being processed (validation + policy execution)
- `"EXPIRED"` — Verification request expired without being utilized
- `"SUCCESSFUL"` — Verification completed fully successfully (all validation & policies passed)
- `"FAILED"` — Verification was unsuccessful (presentation validation or policies failed)
- **attempted**: _Boolean_ - Whether the wallet has attempted to respond to the verification request
- **reattemptable**: _Boolean_ - Whether the user can retry this verification session if it failed
- **policies**: _Object_ - The policies configured for this session:
- **vp_policies**: _Object_ (optional) - Policies to run on presentations
- **vc_policies**: _Array_ (optional) - Policies to run on credentials
- **specific_vc_policies**: _Object_ (optional) - Policies to run on specific credential IDs
- **policy_results**: _Object_ (optional) - Detailed results for all policies applied to the verification (only present after presentation is received):
- **vp_policies**: _Object_ - Results for presentation-level policies (audience check, nonce check, envelope signature), organized by credential ID. Structure: `{ "credential_id": { "policy_name": { ... } } }`
- **vc_policies**: _Array_ - Results for credential-level policies (signature, expiry, schema, etc.). Each item contains `policy`, `success`, and `result`.
- **specific_vc_policies**: _Object_ - Results for credential-specific policies
- **overallSuccess**: _Boolean_ (computed) - Whether all policies passed
- **redirects**: _Object_ (optional) - Success and error redirect URIs if configured
**Note:**
Instead of polling the session status, you can use **callbacks** or **Server Sent Events (SSE)** to be notified when the verification completes. See [Callbacks & SSE](https://docs.walt.id/community-stack/verifier2/callback-and-sse.md) for details.
## Example 2: Requesting Multiple Credentials
You can request multiple credentials in a single verification session. The wallet will present all matching credentials, and policies will be applied to each one.
This example requests both an Identity and a Education credential.
**Option: CURL**
**Endpoint:** `POST /verification-session/create` | [API Reference](https://verifier2.portal.test.waltid.cloud/swagger/index.html#/Verification%20Session%20Management/post_verification_session_create)
##### Example Request
```bash
curl -X 'POST' \
'http://localhost:7004/verification-session/create' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"flow_type": "cross_device",
"core_flow": {
"dcql_query": {
"credentials": [
{
"id": "identity",
"format": "dc+sd-jwt",
"meta": {
"vct_values": [
"https://issuer.demo.walt.id/draft13/identity_credential"
]
}
},
{
"id": "education",
"format": "dc+sd-jwt",
"meta": {
"vct_values": [
"https://issuer.demo.walt.id/draft13/educational_credential"
]
}
}
]
},
"policies": {
"vc_policies": [
{
"policy": "signature"
}
]
}
}
}'
```
The wallet will present both credentials (if available), and the signature policy will be applied to each one. The `policyResults` in the session info response will contain separate results for each credential using their `id` values.
## Understanding Flow Types and Core Concepts
Now that you've seen verification in action, let's understand the underlying structure.
### Verification Session Structure
A `VerificationSession` defines the full lifecycle: authorization request → user presentation → policy validation → final result.
Every verification request follows this structure:
```json
{
"flow_type": "cross_device | same_device | dc_api",
"core_flow": { /* common to all flows */ },
"url_config": { /* specific to cross_device and same_device */ },
"redirects": { /* optional */ }
}
```
**Why this structure?**
- **flow_type** determines how the credential request is delivered (QR code vs deep link vs browser API)
- **core_flow** contains the credential query and policies (same regardless of delivery method)
- **url_config** and **redirects** are flow-specific attributes
This separation makes it easy to switch between flows without changing your credential requirements.
### Flow Type Comparison
| Flow Type | Use Case | Required Attributes | User Experience |
|-----------|----------|---------------------|-----------------|
| `cross_device` | User scans QR code from desktop | None beyond core flow | Desktop shows QR → User scans with phone → Phone wallet opens |
| `same_device` | User clicks link on mobile | `redirects` (success/error URLs) | Browser redirects to wallet → Wallet opens → User returns to browser |
| `dc_api` | Browser-native credential exchange | `expectedOrigins` (array of allowed origins) | Browser shows native credential picker → User selects → Instant return |
## Configuration Defaults
Many session parameters can be configured globally in `verifier-service.conf` and overridden per-request when needed.
| Parameter | Config File Location | Request Override Path | When to Override |
|-----------|---------------------|----------------------|------------------|
| `urlPrefix` | `verifier-service.conf` → `urlPrefix` | `url_config.url_prefix` | Testing with ngrok, different environments per session |
| `urlHost` | `verifier-service.conf` → `urlHost` | `url_config.url_host` | Custom authorization endpoints, conformance testing |
| `clientId` | `verifier-service.conf` → `clientId` | `core_flow.clientId` | Different verifier identities per use case |
| `clientMetadata` | `verifier-service.conf` → `clientMetadata` | `core_flow.client_metadata` | Session-specific branding, multi-tenant scenarios |
| Signing key | `verifier-service.conf` → `key` | `core_flow.key` | Different signing keys per session (advanced) |
| Certificate chain | `verifier-service.conf` → `x5c` | `core_flow.x5c` | Different certificate chains per session (x509 flows) |
**Note:**
**Best Practice:** Set defaults in `verifier-service.conf` for your primary use case, and override per-request only when necessary. This keeps your API calls clean and reduces duplication.
See [Verifier Service Configuration](https://docs.walt.id/community-stack/verifier2/configurations/config-files/verifier-service.md) for detailed configuration documentation.
## Advanced Options
### Requesting Specific Claims
SD-JWT VCs support selective disclosure. You can request specific claims using the `claims` array:
```json
{
"flow_type": "cross_device",
"core_flow": {
"dcql_query": {
"credentials": [
{
"id": "credential_1",
"format": "dc+sd-jwt",
"meta": {
"vct_values": [
"https://issuer.demo.walt.id/draft13/identity_credential"
]
},
"claims": [
{
"path": ["given_name"]
},
{
"path": ["family_name"]
},
{
"path": ["address", "street_address"]
}
]
}
]
}
}
}
```
This requests only `given_name`, `family_name`, and `address.street_address` from the identity credential. The user's wallet will only disclose these specific claims.
### Custom Policies
Override default validation policies:
```json
{
"flow_type": "cross_device",
"core_flow": {
"dcql_query": { /* ... */ },
"policies": {
"vc_policies": [
{
"policy": "signature"
},
{
"policy": "expired"
},
{
"policy": "not-before"
}
]
}
}
}
```
See [Policies overview](https://docs.walt.id/community-stack/verifier2/policies/overview.md) for all available policies.
### Transaction Data Authorization
Verifier2 supports OpenID4VP `transaction_data` to bind credential presentations to specific transactions (e.g., payment authorization, account access).
For SD-JWT VC credentials, the wallet signs the requested transaction data hashes in the KB-JWT, and the verifier evaluates the `dc+sd-jwt/transaction-data-hash-check` policy automatically to validate the binding.
#### Example Request
```json
{
"flow_type": "cross_device",
"core_flow": {
"dcql_query": {
"credentials": [
{
"id": "pid",
"format": "dc+sd-jwt",
"meta": {
"vct_values": [
"https://issuer.demo.walt.id/draft13/identity_credential"
]
},
"claims": [
{ "path": ["given_name"] },
{ "path": ["family_name"] }
]
}
]
}
},
"openid": {
"transactionData": [
{
"type": "org.waltid.transaction-data.payment-authorization",
"credential_ids": ["pid"],
"require_cryptographic_holder_binding": true,
"transaction_data_hashes_alg": ["sha-256"],
"amount": "42.00",
"currency": "EUR",
"payee": "ACME Corp"
}
]
}
}
```
#### Parameters
- **type**: _String_ (required) - The transaction data type identifier. Must match a profile registered via the [`transaction-data-profiles` configuration](https://docs.walt.id/community-stack/verifier2/configurations/config-files/transaction-data-profiles.md). Common types:
- `"org.waltid.transaction-data.payment-authorization"` — For payment transactions (fields: `amount`, `currency`, `payee`)
- `"org.waltid.transaction-data.account-access"` — For account access requests (fields: `account_identifier`, `access_scope`)
- **credential_ids**: _Array of Strings_ (required) - References one or more credential IDs from your DCQL query (`credentials[].id`). The wallet will bind this transaction data to presentations of these credentials.
- **require_cryptographic_holder_binding**: _Boolean_ (optional) - If provided, it must be set to `true`. Transaction data requires cryptographic holder binding (KB-JWT for SD-JWT VC, DeviceAuth for mDL). Requests with `false` are rejected per [OpenID4VP §B.3.3](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#appendix-B.3.3).
- **transaction_data_hashes_alg**: _Array of Strings_ (optional) — Hash algorithms the wallet uses to bind transaction data. e.g. `["sha-256"]`. If omitted "sha-256" is used.
**Type-Specific Parameters:**
For `"org.waltid.transaction-data.payment-authorization"`:
- **amount**: _String_ (required) - Payment amount (e.g., `"42.00"`)
- **currency**: _String_ (required) - Currency code (e.g., `"EUR"`, `"USD"`)
- **payee**: _String_ (required) - Payee identifier (e.g., `"ACME Corp"`)
For `"org.waltid.transaction-data.account-access"`:
- **account_identifier**: _String_ (required) - Account identifier (e.g., account number, user ID)
- **access_scope**: _String_ (required) - Scope of access being requested (e.g., `"read"`, `"write"`, `"admin"`)
**Note:**
**Encoding Behavior:** You pass transaction data entries as plain JSON objects in your verification request. The verifier service automatically base64url-encodes each entry before placing it in the authorization request sent to the wallet. Do not base64-encode the entries yourself.
#### Constraints
- `transaction_data` requires a `dcql_query`. Requests without one are rejected with `invalid_request`.
- Each entry's `credential_ids` must reference DCQL credential query ids declared in the same request.
- Each referenced credential query must use a transaction-data-capable format: `dc+sd-jwt` or `mso_mdoc`.
- Transaction data with `require_cryptographic_holder_binding: false` is rejected. See [OpenID4VP §B.3.3](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#appendix-B.3.3).
- Transaction-data types are configurable via profiles (e.g. `payment-authorization`, `account-access`). Custom types can be added through the [`transaction-data-profiles` configuration](https://docs.walt.id/community-stack/verifier2/configurations/config-files/transaction-data-profiles.md), and the active profiles are exposed via `GET /transaction-data-profiles`.
- Available on `cross_device`, `same_device`, and `dc_api` (Annex D) flows via the `openid.transactionData` field.
**Note:**
**Automatic Policy Enforcement:** When `transactionData` is present, the verifier automatically adds the appropriate transaction-data hash-check policy (`dc+sd-jwt/transaction-data-hash-check` for SD-JWT, `mso_mdoc/transaction-data-hash-check` for mDL) to ensure the wallet properly binds the transaction data. This happens even if you specify custom `vp_policies`. Your custom policies are preserved; the hash-check policy is added on top.
#### Discovery & Configuration
Transaction data types are configurable via profiles. To see which transaction data types your verifier instance supports:
```bash
curl -X 'GET' \
'http://localhost:7004/transaction-data-profiles' \
-H 'accept: application/json'
```
Custom transaction data types can be added through the [`transaction-data-profiles` configuration](https://docs.walt.id/community-stack/verifier2/configurations/config-files/transaction-data-profiles.md).
### Signed Requests
Sign the authorization request JWT using a verifier signing key. This is required for:
- **HAIP profile** compliance
- **DC API Annex C** (ISO 18013-7) flows
Enable by setting `signedRequest: true` in the request body.
**Key Configuration Required:** Signed requests require a signing key (`key`) to be configured. You can either:
- Set the `key` parameter in the [`verifier-service.conf`](https://docs.walt.id/community-stack/verifier2/configurations/config-files/verifier-service.md#key-optional) as a service-wide default
- Override or provide the `key` directly in the verification request body
**Certificate Chain (`x5c`) Required for:**
- X.509-based client authentication when using `x509_san_dns:` or `x509_hash:` as the `clientId` format
- Signed Annex C (ISO 18013-7) requests
The `x5c` parameter can be configured the same way as `key` — as a service-wide default or per-session override.
### Encrypted Responses
The wallet can encrypt the VP token response. Enable by setting `encryptedResponse: true` in the request body.
**Key Configuration Required:** Encrypted responses require a key (`key`) for decrypting the VP token response. Configure it via [`verifier-service.conf`](https://docs.walt.id/community-stack/verifier2/configurations/config-files/verifier-service.md#key-optional) or provide it in the verification request body.
## Next Steps
- **Apply custom policies** — See [Policies](https://docs.walt.id/community-stack/verifier2/policies/overview.md) for validation rules beyond signature verification
- **Set up notifications** — Use [Callbacks & SSE](https://docs.walt.id/community-stack/verifier2/callback-and-sse.md) instead of polling for session status
- **Verify other credential types** — Try [W3C VC](https://docs.walt.id/community-stack/verifier2/credential-verification/vc-oid4vp.md) or [mDL](https://docs.walt.id/community-stack/verifier2/credential-verification/mdl-oid4vp.md)
- **Use DC API** — Try the [Digital Credentials API](https://docs.walt.id/community-stack/verifier2/credential-verification/dc-api.md) for browser-native credential exchange
--------------------------------------------------------------------------------
PAGE: Verifying Mobile Driver's Licenses (mDL, ISO/IEC 18013-5) via OID4VP
URL: https://docs.walt.id/community-stack/verifier2/credential-verification/mdl-oid4vp
RAW: https://docs.walt.id/community-stack/verifier2/credential-verification/mdl-oid4vp.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Verifying Mobile Driver's Licenses (mDL, ISO/IEC 18013-5) via OID4VP
Video: https://youtu.be/G2qFmwOhTwA
This guide shows you how to verify ISO/IEC 18013-5 [**Mobile Driver's License (mDL)**](https://docs.walt.id/concepts/digital-credentials/mdoc-mdl-iso.md) using the walt.id Verifier2 API and the [**OpenID for Verifiable Presentations (OID4VP)**](https://docs.walt.id/concepts/data-exchange-protocols/openid4vp.md) protocol. The verification process sends a credential request to a holder's wallet, receives the signed presentation, and validates it against your policies.
[**mDL (Mobile Driver's License)**](https://docs.walt.id/concepts/digital-credentials/mdoc-mdl-iso.md): A digital representation of a driver's license based on the [ISO/IEC 18013-5 standard](https://www.iso.org/standard/69084.html), stored securely on digital ID wallet.
[**OID4VP**](https://docs.walt.id/concepts/data-exchange-protocols/openid4vp.md): A protocol specifying how parties can present digital credentials in a way that's consistent and secure across platforms ensuring interoperability.
**Note:**
If you're migrating from the original Verifier (v1), see the [Migration Guide](https://docs.walt.id/community-stack/verifier2/migration-from-verifier1.md).
## Prerequisites
Before you begin, ensure you have:
- **Verifier2 API running** — Follow the [Getting Started](https://docs.walt.id/community-stack/verifier2/getting-started.md) or [Setup](https://docs.walt.id/community-stack/verifier2/setup.md) guide
- **A test wallet with credentials** — Use the [walt.id Wallet](https://docs.walt.id/community-stack/wallet/getting-started.md) for testing, or any OID4VP-compatible wallet
- **Publicly accessible URL** (for local development) — Use [ngrok](https://ngrok.com) or similar to expose `http://localhost:7004`.
**Note:**
**Local Development:** When running locally via Docker, the API is available at `http://localhost:7004` by default, whether standalone or via Docker Compose. Use this as your base URL in the examples below.
---
## Example 1: Basic Verification
This example requests a mobile driver's license (mDL) and verifies its cryptographic signature using the default signature policy.
**Option: CURL**
**Endpoint:** `POST /verification-session/create` | [API Reference](https://verifier2.portal.test.waltid.cloud/swagger/index.html#/Verification%20Session%20Management/post_verification_session_create)
##### Example Request
```bash
curl -X 'POST' \
'http://localhost:7004/verification-session/create' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"flow_type": "cross_device",
"core_flow": {
"dcql_query": {
"credentials": [
{
"id": "credential_1",
"format": "mso_mdoc",
"meta": {
"doctype_value": "org.iso.18013.5.1.mDL"
},
"claims": [
{
"path": [
"org.iso.18013.5.1",
"family_name"
]
},
{
"path": [
"org.iso.18013.5.1",
"given_name"
]
},
{
"path": [
"org.iso.18013.5.1",
"birth_date"
]
}
]
}
]
},
"policies": {
"vc_policies": [
{
"policy": "signature"
}
]
}
}
}'
```
**Body Parameters**
- **flow_type**: _String_ (required) - Specifies how the credential request will be delivered to the holder's wallet. Options:
- `"cross_device"` — User scans a QR code on a different device (e.g. desktop browser → phone wallet)
- `"same_device"` — User clicks a deep link on the same device (e.g. phone browser → wallet app on same phone)
- `"dc_api_openid4vp"` — Browser/OS-native Digital Credentials API (Chrome, Safari, iOS/Android native) using OpenID4VP.
- `"dc_api_18013_7"` — Browser/OS-native Digital Credentials API (Chrome, Safari, iOS/Android native) using ISO 18013-7.
- **core_flow**: _Object_ (required) - Defines what credentials to request and which policies to apply. Contains:
- **dcql_query**: _Object_ (required) - Specifies the credential type, format, and claims to request using the [DCQL query language](https://docs.walt.id/concepts/data-exchange-protocols/dcql.md). You can also use the DCQL builder [here](https://dcql.walt.id/).
- **credentials**: _Array_ - List of credential queries. Each credential query contains:
- **id**: _String_ - Unique identifier for this credential in the request (used in policy results)
- **format**: _String_ - Credential format. For mDL, use `"mso_mdoc"` (mobile security object document).
- **meta.doctype_value**: _String_ - The ISO mDL document type. For mobile driver's licenses, use `"org.iso.18013.5.1.mDL"`.
- **claims**: _Array_ (optional) - Specific claims to request from the credential. Each claim has a `path` array with namespace and claim name (e.g. `["org.iso.18013.5.1", "family_name"]`). If omitted, all claims that are non-selectively disclosable are provided by the wallet.
- **policies**: _Object_ (optional) - Validation rules applied to the received credential. Defaults to signature verification only if omitted.
- **vc_policies**: _Array_ - Policies applied to individual credentials. Each policy has a `policy` field specifying the policy name. Available policies: `signature`, `expired`, `not-before`, `revoked-status-list`, `schema`, and more. See [Policies](https://docs.walt.id/community-stack/verifier2/policies/overview.md) for the complete list.
- **vp_policies**: _Array_ (optional) - Policies applied to the entire presentation (not shown in this example)
- **signedRequest**: _Boolean_ (optional) - Whether to sign the authorization request JWT. Required for HAIP profile compliance and DC API Annex C (ISO 18013-7) flows. Defaults to `false`. See [Signed Requests](#signed-requests) to learn more.
- **encryptedResponse**: _Boolean_ (optional) - Whether the wallet should encrypt the VP token response. Defaults to `false`. See [Encrypted Responses](#encrypted-responses) to learn more.
- **key**: _Object_ (optional) - Signing key for this verification session. Overrides the service-wide default `key` configured via the[`verifier-service.conf`](https://docs.walt.id/community-stack/verifier2/configurations/config-files/verifier-service.md#key-optional). Must be a serialized key in the format `{"type":"jwk","jwk":{...}}`. Required when `signedRequest: true` or `encryptedResponse: true` and no service-wide default is configured. See [Signed Requests](#signed-requests) and [Encrypted Responses](#encrypted-responses) for when this is needed.
- **x5c**: _Array of Strings_ (optional) - X.509 certificate chain for this verification session. Overrides the service-wide default `x5c` configured via [`verifier-service.conf`](https://docs.walt.id/community-stack/verifier2/configurations/config-files/verifier-service.md#x5c-optional). Must be an ordered chain in DER-encoded, Base64-encoded format where the first certificate corresponds to the `key`. Required for X.509-based client authentication (`x509_san_dns:` or `x509_hash:` as `clientId`) and signed Annex C requests. See [Signed Requests](#signed-requests) to learn more.
- **verifier_info**: _Object_ (optional) - An array of attestations about the Verifier relevant to the Credential Request. Each object is a JSON object with the following fields:
- **format**: _String_ - The format of the attestation. Currently only `"jwt"` is supported.
- **data**: _String_ - The attestation data in JWT format.
- **credentialIds**: _Array_ (optional) - Specifies which credentials in your DCQL query this attestation applies to. References the `id` fields from your `credentials` array in the DCQL query. If omitted, the attestation applies to all requested credentials.
- **openid**: _Object_ (optional) - OpenID4VP-specific extensions
- **transactionData**: _Array_ (optional) - Bind the credential presentation to a specific transaction (e.g., payment authorization, account access). Each entry references credential IDs and includes transaction details. See [Transaction Data Authorization](#transaction-data-authorization) for full documentation.
- **url_config**: _Object_ (optional) - URL configuration for this session. Only applies to `cross_device` and `same_device` flows (not `dc_api`).
- **url_prefix**: _String_ (optional) - The publicly accessible base URL where the wallet will fetch the authorization request and post its response. Overrides the `urlPrefix` from `verifier-service.conf`.
- The service constructs: Request URI: `{urlPrefix}/{sessionId}/request` and Response URI: `{urlPrefix}/{sessionId}/response`
- **Must be reachable by the wallet** (use ngrok or public URL for local development)
- **url_host**: _String_ (optional) - The base URL used to construct the authorization request link. Overrides the `urlHost` from `verifier-service.conf`.
- For standard OID4VP flows, use `"openid4vp://authorize"` (default)
- For custom authorization endpoints (e.g. conformance testing), provide a full HTTPS URL
- **redirects**: _Object_ (optional) - Success and error redirect URLs (only for `cross_device` and `same_device` flows)
- **success_redirect_uri**: _String_ - URL to redirect the user after successful verification
- **error_redirect_uri**: _String_ - URL to redirect the user if verification fails
**Note:**
Many parameters have defaults configured in `verifier-service.conf`. Request-level parameters override these defaults for that specific session only. See [Configuration Defaults](#configuration-defaults) below.
---
##### Example Response
The response contains the session ID and authorization URLs to present to the user.
```json
{
"sessionId": "4a803c4b-1dc3-4210-806d-3fb52f1780c8",
"bootstrapAuthorizationRequestUrl": "openid4vp://authorize?client_id=verifier2&request_uri=https%3A%2F%2F575e-2a02-8388-17c1-5580-b97f-5031-3eee-c8a4.ngrok-free.app%2Fverification-session%2F4a803c4b-1dc3-4210-806d-3fb52f1780c8%2Frequest",
"fullAuthorizationRequestUrl": "openid4vp://authorize?response_type=vp_token&client_id=verifier2&state=da82aed7-ea36-4303-9c4a-3ded03cdedb4&response_mode=direct_post&nonce=74b0efc4-331c-488f-9489-7ce72b1c698f&response_uri=https%3A%2F%2F575e-2a02-8388-17c1-5580-b97f-5031-3eee-c8a4.ngrok-free.app%2Fverification-session%2F4a803c4b-1dc3-4210-806d-3fb52f1780c8%2Fresponse&dcql_query=%7B%22credentials%22%3A%5B%7B%22id%22%3A%22credential_1%22%2C%22format%22%3A%22mso_mdoc%22%2C%22meta%22%3A%7B%22doctype_value%22%3A%22org.iso.18013.5.1.mDL%22%7D%2C%22claims%22%3A%5B%7B%22path%22%3A%5B%22org.iso.18013.5.1%22%2C%22family_name%22%5D%7D%2C%7B%22path%22%3A%5B%22org.iso.18013.5.1%22%2C%22given_name%22%5D%7D%2C%7B%22path%22%3A%5B%22org.iso.18013.5.1%22%2C%22birth_date%22%5D%7D%5D%7D%5D%7D&client_metadata=%7B%22vp_formats_supported%22%3A%7B%22mso_mdoc%22%3A%7B%22issuerauth_alg_values%22%3A%5B-7%2C-9%2C-50%5D%2C%22deviceauth_alg_values%22%3A%5B-7%2C-9%2C-50%2C-65537%5D%7D%7D%2C%22client_name%22%3A%22Verifier2%22%2C%22logo_uri%22%3A%22https%3A%2F%2Fimages.squarespace-cdn.com%2Fcontent%22%2C%22tos_uri%22%3A%22https%3A%2F%2Fexample.com%2Fterms%22%2C%22policy_uri%22%3A%22https%3A%2F%2Fexample.com%2Fprivacy%22%2C%22client_uri%22%3A%22https%3A%2F%2Fverifier.example.com%22%2C%22client_name%23fr-FR%22%3A%22V%C3%A9rificateur2%22%2C%22logo_uri%23fr-FR%22%3A%22https%3A%2F%2Fexample.com%2Flogo-fr.png%22%2C%22tos_uri%23fr-FR%22%3A%22https%3A%2F%2Fexample.com%2Fterms-fr%22%2C%22policy_uri%23fr-FR%22%3A%22https%3A%2F%2Fexample.com%2Fprivacy-fr%22%2C%22client_uri%23fr-FR%22%3A%22https%3A%2F%2Fverifier.example.com%2Ffr%22%7D"
}
```
**Response Fields:**
- **sessionId**: _String_ - The unique ID of this verification session. Use this to query the session status.
- **bootstrapAuthorizationRequestUrl**: _String_ - Shortened URL using `request_uri` by reference. **Use this for QR codes** to minimize QR code density.
- **fullAuthorizationRequestUrl**: _String_ - Complete URL with all parameters embedded. Use this if the wallet doesn't support `request_uri` (rare).
## Presenting the Request to the User
After creating the session, you need to present the authorization request to the user based on the flow type you chose.
### Cross-Device Flow (QR Code)
1. **Generate a QR code** from the `bootstrapAuthorizationRequestUrl`
2. **Display the QR code** to the user on your web page or application
3. **User scans the QR code** with their wallet app
4. **Wallet presents the credential** and posts the response to your verifier
5. **Monitor the session status** by polling the session endpoint or using SSE/callbacks (see below)
### Same-Device Flow (Deep Link)
1. **Redirect the user** to the `bootstrapAuthorizationRequestUrl`
2. **User's wallet app opens** automatically (via deep link)
3. **Wallet presents the credential** and posts the response to your verifier
4. **User is redirected** to your `successRedirectUri` or `errorRedirectUri`
5. **Query the session status** to retrieve the verification results
---
## Checking Verification Results
You can query the verification session to check if the user has presented their credential and whether validation succeeded.
**Option: CURL**
**Endpoint:** `GET /verification-session/{sessionId}/info` | [API Reference](https://verifier2.portal.test.waltid.cloud/swagger/index.html#/Verification%20Session%20Management/get_verification_session__sessionId__info)
##### Example Request
```bash
curl -X 'GET' \
'http://localhost:7004/verification-session/{sessionId}/info' \
-H 'accept: application/json'
```
**Path Parameters**
- **sessionId**: _String_ (required) - The unique ID of the verification session returned when you created the session. Use this to query the current status and results.
---
##### Example Response
The response contains the complete session information including setup, status, policy results, and presented credentials.
```json
{
"id": "4a803c4b-1dc3-4210-806d-3fb52f1780c8",
"setup": {
"flow_type": "cross_device",
"core_flow": {
"dcql_query": {
"credentials": [
{
"id": "credential_1",
"format": "mso_mdoc",
"multiple": false,
"meta": {
"doctype_value": "org.iso.18013.5.1.mDL",
"format": "mso_mdoc"
},
"require_cryptographic_holder_binding": true,
"claims": [
{
"path": [
"org.iso.18013.5.1",
"family_name"
]
},
{
"path": [
"org.iso.18013.5.1",
"given_name"
]
},
{
"path": [
"org.iso.18013.5.1",
"birth_date"
]
}
]
}
]
},
"signed_request": false,
"encrypted_response": false,
"policies": {
"vc_policies": [
{
"policy": "signature",
"id": "signature"
}
]
}
},
"url_config": {}
},
"creationDate": "2026-05-18T07:48:39.335523256Z",
"expirationDate": "2026-05-18T07:53:39.335523256Z",
"retentionDate": "2036-05-18T07:48:39.335523256Z",
"status": "SUCCESSFUL",
"attempted": true,
"reattemptable": true,
"bootstrapAuthorizationRequest": {
"client_id": "verifier2",
"request_uri": "https://575e-2a02-8388-17c1-5580-b97f-5031-3eee-c8a4.ngrok-free.app/verification-session/4a803c4b-1dc3-4210-806d-3fb52f1780c8/request"
},
"bootstrapAuthorizationRequestUrl": "openid4vp://authorize?client_id=verifier2&request_uri=https%3A%2F%2F575e-2a02-8388-17c1-5580-b97f-5031-3eee-c8a4.ngrok-free.app%2Fverification-session%2F4a803c4b-1dc3-4210-806d-3fb52f1780c8%2Frequest",
"authorizationRequest": {
"response_type": "vp_token",
"client_id": "verifier2",
"state": "da82aed7-ea36-4303-9c4a-3ded03cdedb4",
"response_mode": "direct_post",
"nonce": "74b0efc4-331c-488f-9489-7ce72b1c698f",
"response_uri": "https://575e-2a02-8388-17c1-5580-b97f-5031-3eee-c8a4.ngrok-free.app/verification-session/4a803c4b-1dc3-4210-806d-3fb52f1780c8/response",
"dcql_query": {
"credentials": [
{
"id": "credential_1",
"format": "mso_mdoc",
"multiple": false,
"meta": {
"doctype_value": "org.iso.18013.5.1.mDL",
"format": "mso_mdoc"
},
"require_cryptographic_holder_binding": true,
"claims": [
{
"path": [
"org.iso.18013.5.1",
"family_name"
]
},
{
"path": [
"org.iso.18013.5.1",
"given_name"
]
},
{
"path": [
"org.iso.18013.5.1",
"birth_date"
]
}
]
}
]
},
"client_metadata": {
"vp_formats_supported": {
"mso_mdoc": {
"issuerauth_alg_values": [
-7,
-9,
-50
],
"deviceauth_alg_values": [
-7,
-9,
-50,
-65537
]
}
},
"client_name": "Verifier2",
"logo_uri": "https://images.squarespace-cdn.com/content",
"tos_uri": "https://example.com/terms",
"policy_uri": "https://example.com/privacy",
"client_uri": "https://verifier.example.com",
"client_name#fr-FR": "Vérificateur2",
"logo_uri#fr-FR": "https://example.com/logo-fr.png",
"tos_uri#fr-FR": "https://example.com/terms-fr",
"policy_uri#fr-FR": "https://example.com/privacy-fr",
"client_uri#fr-FR": "https://verifier.example.com/fr"
}
},
"authorizationRequestUrl": "openid4vp://authorize?response_type=vp_token&client_id=verifier2&state=da82aed7-ea36-4303-9c4a-3ded03cdedb4&response_mode=direct_post&nonce=74b0efc4-331c-488f-9489-7ce72b1c698f&response_uri=https%3A%2F%2F575e-2a02-8388-17c1-5580-b97f-5031-3eee-c8a4.ngrok-free.app%2Fverification-session%2F4a803c4b-1dc3-4210-806d-3fb52f1780c8%2Fresponse&dcql_query=%7B%22credentials%22%3A%5B%7B%22id%22%3A%22credential_1%22%2C%22format%22%3A%22mso_mdoc%22%2C%22meta%22%3A%7B%22doctype_value%22%3A%22org.iso.18013.5.1.mDL%22%7D%2C%22claims%22%3A%5B%7B%22path%22%3A%5B%22org.iso.18013.5.1%22%2C%22family_name%22%5D%7D%2C%7B%22path%22%3A%5B%22org.iso.18013.5.1%22%2C%22given_name%22%5D%7D%2C%7B%22path%22%3A%5B%22org.iso.18013.5.1%22%2C%22birth_date%22%5D%7D%5D%7D%5D%7D&client_metadata=%7B%22vp_formats_supported%22%3A%7B%22mso_mdoc%22%3A%7B%22issuerauth_alg_values%22%3A%5B-7%2C-9%2C-50%5D%2C%22deviceauth_alg_values%22%3A%5B-7%2C-9%2C-50%2C-65537%5D%7D%7D%2C%22client_name%22%3A%22Verifier2%22%2C%22logo_uri%22%3A%22https%3A%2F%2Fimages.squarespace-cdn.com%2Fcontent%22%2C%22tos_uri%22%3A%22https%3A%2F%2Fexample.com%2Fterms%22%2C%22policy_uri%22%3A%22https%3A%2F%2Fexample.com%2Fprivacy%22%2C%22client_uri%22%3A%22https%3A%2F%2Fverifier.example.com%22%2C%22client_name%23fr-FR%22%3A%22V%C3%A9rificateur2%22%2C%22logo_uri%23fr-FR%22%3A%22https%3A%2F%2Fexample.com%2Flogo-fr.png%22%2C%22tos_uri%23fr-FR%22%3A%22https%3A%2F%2Fexample.com%2Fterms-fr%22%2C%22policy_uri%23fr-FR%22%3A%22https%3A%2F%2Fexample.com%2Fprivacy-fr%22%2C%22client_uri%23fr-FR%22%3A%22https%3A%2F%2Fverifier.example.com%2Ffr%22%7D",
"requestMode": "REQUEST_URI",
"policies": {
"vp_policies": {
"jwt_vc_json": [
"jwt_vc_json/audience-check",
"jwt_vc_json/nonce-check",
"jwt_vc_json/envelope_signature"
],
"dc+sd-jwt": [
"dc+sd-jwt/audience-check",
"dc+sd-jwt/kb-jwt_signature",
"dc+sd-jwt/nonce-check",
"dc+sd-jwt/sd_hash-check"
],
"mso_mdoc": [
"mso_mdoc/device-auth",
"mso_mdoc/device_key_auth",
"mso_mdoc/issuer_auth",
"mso_mdoc/issuer_signed_integrity",
"mso_mdoc/mso"
]
},
"vc_policies": [
{
"policy": "signature",
"id": "signature"
}
]
},
"policy_results": {
"vp_policies": {
"credential_1": {
"mso_mdoc/device-auth": {
"policy_executed": {
"policy": "mso_mdoc/device-auth",
"id": "mso_mdoc/device-auth",
"description": "Verify device authentication"
},
"success": true,
"results": {
"device_public_jwk": {
"kty": "OKP",
"crv": "Ed25519",
"x": "jxQwRV7tHc3W_JkKVsDZL7_DtP4zyNJtQ4Yr1z7-Md4"
},
"device_auth_bytes_hex": "d8185868847444657669636541757468656e7469636174696f6e83f6f682714f70656e494434565048616e646f7665725820bdb670da65dae163a8fe31c591c82da53c6da1b38232c8f4d3657405e4220a43756f72672e69736f2e31383031332e352e312e6d444cd81841a0"
},
"errors": [],
"execution_time": "PT0.002658542S"
},
"mso_mdoc/device_key_auth": {
"policy_executed": {
"policy": "mso_mdoc/device_key_auth",
"id": "mso_mdoc/device_key_auth",
"description": "Verify holder-verified data"
},
"success": true,
"results": {
"empty_device_signed_namespaces": true
},
"errors": [],
"execution_time": "PT0.000800334S"
},
"mso_mdoc/issuer_auth": {
"policy_executed": {
"policy": "mso_mdoc/issuer_auth",
"id": "mso_mdoc/issuer_auth",
"description": "Verify issuer authentication"
},
"success": true,
"results": {
"certificate_chain": [
"3082022b308201d1a0030201020214440fe81f511c691ef21f46d51ef752bbde666996300a06082a8648ce3d040302302c310b3009060355040613024154311d301b06035504030c146d444c20497373756572204941434120526f6f74301e170d3236303531383036313932365a170d3237303831383036313932365a3032310b30090603550406130241543123302106035504030c1a6d444c2049737375657220446f63756d656e74205369676e65723059301306072a8648ce3d020106082a8648ce3d03010703420004c9f99bb42b1d410ff81033e0d06b7df20258935ce434ea4633d6c76048b8f667301080b22778927ef1e8a3f7d5bc33469d3cd03e9795b4761b19a673bec228f9a381ca3081c7301f0603551d2304183016801472b2f76f49d0bcb3dd70cc2d6499a3378002e8e3301d0603551d0e041604145c64d6b94b4538b14534525b3942eef8f841827d300e0603551d0f0101ff04040302078030270603551d120420301e861c68747470733a2f2f696163612e74656e616e74312e77616c742e696430150603551d250101ff040b3009060728818c5d05010230350603551d1f042e302c302aa028a026862468747470733a2f2f63726c2e74656e616e74312e77616c742e69642f696163612e63726c300a06082a8648ce3d0403020348003045022006c7eaed38d41d53d35224d5b420ca41109d5f3a7afde473a8e287ed8688a3d7022100eb7882937823711f9bc909e3f88861436da296b882066ced7fdf66f057f0af39"
],
"signer_pem": "-----BEGIN CERTIFICATE-----\nMIICKzCCAdGgAwIBAgIURA/oH1EcaR7yH0bVHvdSu95maZYwCgYIKoZIzj0EAwIw\r\nLDELMAkGA1UEBhMCQVQxHTAbBgNVBAMMFG1ETCBJc3N1ZXIgSUFDQSBSb290MB4X\r\nDTI2MDUxODA2MTkyNloXDTI3MDgxODA2MTkyNlowMjELMAkGA1UEBhMCQVQxIzAh\r\nBgNVBAMMGm1ETCBJc3N1ZXIgRG9jdW1lbnQgU2lnbmVyMFkwEwYHKoZIzj0CAQYI\r\nKoZIzj0DAQcDQgAEyfmbtCsdQQ/4EDPg0Gt98gJYk1zkNOpGM9bHYEi49mcwEICy\r\nJ3iSfvHoo/fVvDNGnTzQPpeVtHYbGaZzvsIo+aOByjCBxzAfBgNVHSMEGDAWgBRy\r\nsvdvSdC8s91wzC1kmaM3gALo4zAdBgNVHQ4EFgQUXGTWuUtFOLFFNFJbOULu+PhB\r\ngn0wDgYDVR0PAQH/BAQDAgeAMCcGA1UdEgQgMB6GHGh0dHBzOi8vaWFjYS50ZW5h\r\nbnQxLndhbHQuaWQwFQYDVR0lAQH/BAswCQYHKIGMXQUBAjA1BgNVHR8ELjAsMCqg\r\nKKAmhiRodHRwczovL2NybC50ZW5hbnQxLndhbHQuaWQvaWFjYS5jcmwwCgYIKoZI\r\nzj0EAwIDSAAwRQIgBsfq7TjUHVPTUiTVtCDKQRCdXzp6/eRzqOKH7YaIo9cCIQDr\r\neIKTeCNxH5vJCeP4iGFDbaKWuIIGbO1/32bwV/CvOQ==\n-----END CERTIFICATE-----",
"signer_jwk": {
"kty": "EC",
"crv": "P-256",
"x": "yfmbtCsdQQ_4EDPg0Gt98gJYk1zkNOpGM9bHYEi49mc",
"y": "MBCAsid4kn7x6KP31bwzRp080D6XlbR2Gxmmc77CKPk"
}
},
"errors": [],
"execution_time": "PT0.004215542S"
},
"mso_mdoc/issuer_signed_integrity": {
"policy_executed": {
"policy": "mso_mdoc/issuer_signed_integrity",
"id": "mso_mdoc/issuer_signed_integrity",
"description": "Verify issuer-verified data integrity"
},
"success": true,
"results": {
"namespace": {
"org.iso.18013.5.1": [
{
"id": "family_name",
"digest_id": 0,
"value": "Doe",
"value_type": "CborString",
"random_hex": "736b060d21fa4e37f17b0b0568285add9d064ba883eb6c35",
"serialized_hex": "a4686469676573744944006672616e646f6d5818736b060d21fa4e37f17b0b0568285add9d064ba883eb6c3571656c656d656e744964656e7469666965726b66616d696c795f6e616d656c656c656d656e7456616c756563446f65"
},
{
"id": "given_name",
"digest_id": 1,
"value": "John",
"value_type": "CborString",
"random_hex": "312e734c986fdf279c7d75130b0da4794d16d44bbb3cfd61",
"serialized_hex": "a4686469676573744944016672616e646f6d5818312e734c986fdf279c7d75130b0da4794d16d44bbb3cfd6171656c656d656e744964656e7469666965726a676976656e5f6e616d656c656c656d656e7456616c7565644a6f686e"
},
{
"id": "birth_date",
"digest_id": 2,
"value": "1986-03-22",
"value_type": "CborString",
"random_hex": "e6864d13d13a9fb1a7fccb729a6330dec975bac5e79fa023",
"serialized_hex": "a4686469676573744944026672616e646f6d5818e6864d13d13a9fb1a7fccb729a6330dec975bac5e79fa02371656c656d656e744964656e7469666965726a62697274685f646174656c656c656d656e7456616c7565d903ec6a313938362d30332d3232"
}
]
},
"matching_digest": {
"org.iso.18013.5.1": [
"family_name",
"given_name",
"birth_date"
]
}
},
"errors": [],
"execution_time": "PT0.003104834S"
},
"mso_mdoc/mso": {
"policy_executed": {
"policy": "mso_mdoc/mso",
"id": "mso_mdoc/mso",
"description": "Verify MSO"
},
"success": true,
"results": {
"signed": "2026-05-18T07:48:27.870905006Z",
"valid_from": "2027-05-18T07:48:27.852289522Z"
},
"errors": [],
"execution_time": "PT0.004245834S"
}
}
},
"vc_policies": [
{
"policy": {
"policy": "signature",
"id": "signature"
},
"success": true,
"result": {
"verification_result": true,
"signed_credential": "o2d2ZXJzaW9uYzEuMGlkb2N1bWVudHOBo2dkb2NUeXBldW9yZy5pc28uMTgwMTMuNS4xLm1ETGxpc3N1ZXJTaWduZWSiam5hbWVTcGFjZXOhcW9yZy5pc28uMTgwMTMuNS4xg9gYWFukaGRpZ2VzdElEAGZyYW5kb21YGHNrBg0h-k438XsLBWgoWt2dBkuog-tsNXFlbGVtZW50SWRlbnRpZmllcmtmYW1pbHlfbmFtZWxlbGVtZW50VmFsdWVjRG9l2BhYW6RoZGlnZXN0SUQBZnJhbmRvbVgYMS5zTJhv3yecfXUTCw2keU0W1Eu7PP1hcWVsZW1lbnRJZGVudGlmaWVyamdpdmVuX25hbWVsZWxlbWVudFZhbHVlZEpvaG7YGFhkpGhkaWdlc3RJRAJmcmFuZG9tWBjmhk0T0Tqfsaf8y3KaYzDeyXW6xeefoCNxZWxlbWVudElkZW50aWZpZXJqYmlydGhfZGF0ZWxlbGVtZW50VmFsdWXZA-xqMTk4Ni0wMy0yMmppc3N1ZXJBdXRohEOhASahGCFZAi8wggIrMIIB0aADAgECAhRED-gfURxpHvIfRtUe91K73mZpljAKBggqhkjOPQQDAjAsMQswCQYDVQQGEwJBVDEdMBsGA1UEAwwUbURMIElzc3VlciBJQUNBIFJvb3QwHhcNMjYwNTE4MDYxOTI2WhcNMjcwODE4MDYxOTI2WjAyMQswCQYDVQQGEwJBVDEjMCEGA1UEAwwabURMIElzc3VlciBEb2N1bWVudCBTaWduZXIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATJ-Zu0Kx1BD_gQM-DQa33yAliTXOQ06kYz1sdgSLj2ZzAQgLIneJJ-8eij99W8M0adPNA-l5W0dhsZpnO-wij5o4HKMIHHMB8GA1UdIwQYMBaAFHKy929J0Lyz3XDMLWSZozeAAujjMB0GA1UdDgQWBBRcZNa5S0U4sUU0Uls5Qu74-EGCfTAOBgNVHQ8BAf8EBAMCB4AwJwYDVR0SBCAwHoYcaHR0cHM6Ly9pYWNhLnRlbmFudDEud2FsdC5pZDAVBgNVHSUBAf8ECzAJBgcogYxdBQECMDUGA1UdHwQuMCwwKqAooCaGJGh0dHBzOi8vY3JsLnRlbmFudDEud2FsdC5pZC9pYWNhLmNybDAKBggqhkjOPQQDAgNIADBFAiAGx-rtONQdU9NSJNW0IMpBEJ1fOnr95HOo4ofthoij1wIhAOt4gpN4I3Efm8kJ4_iIYUNtopa4ggZs7X_fZvBX8K85WQKV2BhZApCmZ3ZlcnNpb25jMS4wb2RpZ2VzdEFsZ29yaXRobWdTSEEtMjU2bHZhbHVlRGlnZXN0c6Fxb3JnLmlzby4xODAxMy41LjGqAFgglIt2JC3rDbrr6azX5GWPMGyGqyD5gTnyASk37pDhZQYBWCCMQ7Ph45igVnLbvCtlSqLPlaMTXSk2kLoy4Mxn1x37kwJYIOjGjCHBlfvZi74lANZXAWt_FDdDf6NU591eYMGbFladA1ggcdl99l7hzcOFDYUeghGX0gDXYYgEN7WyRFrgSSFVvv4EWCCKwt6H37DEK8kJFcs__AiFZzLDc-4GPFKuYYEJzv_X1QVYIMybLDIPi3G7go7Bq4ggn0UYXEpmKMazxXZH25mnl0RQBlggofgVz7C7i4z0Iz8zrEPhNBRMlZcr4EhqQCR2pjo3qsoHWCDaJ4DszfMYhI0HfyOcSgMEgx_K12dx34khKW6ohKRhZghYIPTE7TeLsXVDSX9l3DTPB4zDhzF1WDTmCw3Ecxv1deNJCVggqueG-PUmdP8sJdsQ6XbuByQzAZXoRdwB-7dn0hxtkCdtZGV2aWNlS2V5SW5mb6FpZGV2aWNlS2V5owEBIAYhWCCPFDBFXu0dzdb8mQpWwNkvv8O0_jPI0m1DhivXPv4x3mdkb2NUeXBldW9yZy5pc28uMTgwMTMuNS4xLm1ETGx2YWxpZGl0eUluZm-jZnNpZ25lZMB4HjIwMjYtMDUtMThUMDc6NDg6MjcuODcwOTA1MDA2Wml2YWxpZEZyb23AeB4yMDI2LTA1LTE4VDA3OjQ4OjI3Ljg3MDkwNTAwNlpqdmFsaWRVbnRpbMB4HjIwMjctMDUtMThUMDc6NDg6MjcuODUyMjg5NTIyWlhAmRC5DQjAwaNaozniXwKmOoMscEyNwA65Y31zyoCJpiGiZxs-fk9bcsZnrDSu5ATPVEIKrf_4hjY8ODArK76q2WxkZXZpY2VTaWduZWSiam5hbWVTcGFjZXPYGEGgamRldmljZUF1dGihb2RldmljZVNpZ25hdHVyZYRDoQEnoPZYQGQUayxOBk2Wp7g7UhumPbtFSQ5h1h4jArb60UGZOsFMvG6MXHBBPd4JdOX40auhbmKNmBjinPfw96QN9XfV-Q9mc3RhdHVzAA",
"credential_signature": {
"type": "signature-cose",
"signerKey": {
"type": "jwk",
"jwk": {
"kty": "EC",
"crv": "P-256",
"x": "yfmbtCsdQQ_4EDPg0Gt98gJYk1zkNOpGM9bHYEi49mc",
"y": "MBCAsid4kn7x6KP31bwzRp080D6XlbR2Gxmmc77CKPk"
}
},
"x5cList": [
"MIICKzCCAdGgAwIBAgIURA/oH1EcaR7yH0bVHvdSu95maZYwCgYIKoZIzj0EAwIwLDELMAkGA1UEBhMCQVQxHTAbBgNVBAMMFG1ETCBJc3N1ZXIgSUFDQSBSb290MB4XDTI2MDUxODA2MTkyNloXDTI3MDgxODA2MTkyNlowMjELMAkGA1UEBhMCQVQxIzAhBgNVBAMMGm1ETCBJc3N1ZXIgRG9jdW1lbnQgU2lnbmVyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEyfmbtCsdQQ/4EDPg0Gt98gJYk1zkNOpGM9bHYEi49mcwEICyJ3iSfvHoo/fVvDNGnTzQPpeVtHYbGaZzvsIo+aOByjCBxzAfBgNVHSMEGDAWgBRysvdvSdC8s91wzC1kmaM3gALo4zAdBgNVHQ4EFgQUXGTWuUtFOLFFNFJbOULu+PhBgn0wDgYDVR0PAQH/BAQDAgeAMCcGA1UdEgQgMB6GHGh0dHBzOi8vaWFjYS50ZW5hbnQxLndhbHQuaWQwFQYDVR0lAQH/BAswCQYHKIGMXQUBAjA1BgNVHR8ELjAsMCqgKKAmhiRodHRwczovL2NybC50ZW5hbnQxLndhbHQuaWQvaWFjYS5jcmwwCgYIKoZIzj0EAwIDSAAwRQIgBsfq7TjUHVPTUiTVtCDKQRCdXzp6/eRzqOKH7YaIo9cCIQDreIKTeCNxH5vJCeP4iGFDbaKWuIIGbO1/32bwV/CvOQ=="
]
},
"verified_data": {
"docType": "org.iso.18013.5.1.mDL",
"org.iso.18013.5.1": {
"family_name": "Doe",
"given_name": "John",
"birth_date": "1986-03-22"
}
},
"successful_issuer_public_key": {
"kty": "EC",
"crv": "P-256",
"x": "yfmbtCsdQQ_4EDPg0Gt98gJYk1zkNOpGM9bHYEi49mc",
"y": "MBCAsid4kn7x6KP31bwzRp080D6XlbR2Gxmmc77CKPk"
},
"successful_issuer_public_key_id": "FOWc2X5ZSle6u66RwXQoVUwm86vnTnbZ61s_Golepz0"
}
}
],
"specific_vc_policies": {},
"overallSuccess": true
},
"presentation_validation_results": {
"credential_1": {
"mso_mdoc/device-auth": {
"policy_executed": {
"policy": "mso_mdoc/device-auth",
"id": "mso_mdoc/device-auth",
"description": "Verify device authentication"
},
"success": true,
"results": {
"device_public_jwk": {
"kty": "OKP",
"crv": "Ed25519",
"x": "jxQwRV7tHc3W_JkKVsDZL7_DtP4zyNJtQ4Yr1z7-Md4"
},
"device_auth_bytes_hex": "d8185868847444657669636541757468656e7469636174696f6e83f6f682714f70656e494434565048616e646f7665725820bdb670da65dae163a8fe31c591c82da53c6da1b38232c8f4d3657405e4220a43756f72672e69736f2e31383031332e352e312e6d444cd81841a0"
},
"errors": [],
"execution_time": "PT0.002658542S"
},
"mso_mdoc/device_key_auth": {
"policy_executed": {
"policy": "mso_mdoc/device_key_auth",
"id": "mso_mdoc/device_key_auth",
"description": "Verify holder-verified data"
},
"success": true,
"results": {
"empty_device_signed_namespaces": true
},
"errors": [],
"execution_time": "PT0.000800334S"
},
"mso_mdoc/issuer_auth": {
"policy_executed": {
"policy": "mso_mdoc/issuer_auth",
"id": "mso_mdoc/issuer_auth",
"description": "Verify issuer authentication"
},
"success": true,
"results": {
"certificate_chain": [
"3082022b308201d1a0030201020214440fe81f511c691ef21f46d51ef752bbde666996300a06082a8648ce3d040302302c310b3009060355040613024154311d301b06035504030c146d444c20497373756572204941434120526f6f74301e170d3236303531383036313932365a170d3237303831383036313932365a3032310b30090603550406130241543123302106035504030c1a6d444c2049737375657220446f63756d656e74205369676e65723059301306072a8648ce3d020106082a8648ce3d03010703420004c9f99bb42b1d410ff81033e0d06b7df20258935ce434ea4633d6c76048b8f667301080b22778927ef1e8a3f7d5bc33469d3cd03e9795b4761b19a673bec228f9a381ca3081c7301f0603551d2304183016801472b2f76f49d0bcb3dd70cc2d6499a3378002e8e3301d0603551d0e041604145c64d6b94b4538b14534525b3942eef8f841827d300e0603551d0f0101ff04040302078030270603551d120420301e861c68747470733a2f2f696163612e74656e616e74312e77616c742e696430150603551d250101ff040b3009060728818c5d05010230350603551d1f042e302c302aa028a026862468747470733a2f2f63726c2e74656e616e74312e77616c742e69642f696163612e63726c300a06082a8648ce3d0403020348003045022006c7eaed38d41d53d35224d5b420ca41109d5f3a7afde473a8e287ed8688a3d7022100eb7882937823711f9bc909e3f88861436da296b882066ced7fdf66f057f0af39"
],
"signer_pem": "-----BEGIN CERTIFICATE-----\nMIICKzCCAdGgAwIBAgIURA/oH1EcaR7yH0bVHvdSu95maZYwCgYIKoZIzj0EAwIw\r\nLDELMAkGA1UEBhMCQVQxHTAbBgNVBAMMFG1ETCBJc3N1ZXIgSUFDQSBSb290MB4X\r\nDTI2MDUxODA2MTkyNloXDTI3MDgxODA2MTkyNlowMjELMAkGA1UEBhMCQVQxIzAh\r\nBgNVBAMMGm1ETCBJc3N1ZXIgRG9jdW1lbnQgU2lnbmVyMFkwEwYHKoZIzj0CAQYI\r\nKoZIzj0DAQcDQgAEyfmbtCsdQQ/4EDPg0Gt98gJYk1zkNOpGM9bHYEi49mcwEICy\r\nJ3iSfvHoo/fVvDNGnTzQPpeVtHYbGaZzvsIo+aOByjCBxzAfBgNVHSMEGDAWgBRy\r\nsvdvSdC8s91wzC1kmaM3gALo4zAdBgNVHQ4EFgQUXGTWuUtFOLFFNFJbOULu+PhB\r\ngn0wDgYDVR0PAQH/BAQDAgeAMCcGA1UdEgQgMB6GHGh0dHBzOi8vaWFjYS50ZW5h\r\nbnQxLndhbHQuaWQwFQYDVR0lAQH/BAswCQYHKIGMXQUBAjA1BgNVHR8ELjAsMCqg\r\nKKAmhiRodHRwczovL2NybC50ZW5hbnQxLndhbHQuaWQvaWFjYS5jcmwwCgYIKoZI\r\nzj0EAwIDSAAwRQIgBsfq7TjUHVPTUiTVtCDKQRCdXzp6/eRzqOKH7YaIo9cCIQDr\r\neIKTeCNxH5vJCeP4iGFDbaKWuIIGbO1/32bwV/CvOQ==\n-----END CERTIFICATE-----",
"signer_jwk": {
"kty": "EC",
"crv": "P-256",
"x": "yfmbtCsdQQ_4EDPg0Gt98gJYk1zkNOpGM9bHYEi49mc",
"y": "MBCAsid4kn7x6KP31bwzRp080D6XlbR2Gxmmc77CKPk"
}
},
"errors": [],
"execution_time": "PT0.004215542S"
},
"mso_mdoc/issuer_signed_integrity": {
"policy_executed": {
"policy": "mso_mdoc/issuer_signed_integrity",
"id": "mso_mdoc/issuer_signed_integrity",
"description": "Verify issuer-verified data integrity"
},
"success": true,
"results": {
"namespace": {
"org.iso.18013.5.1": [
{
"id": "family_name",
"digest_id": 0,
"value": "Doe",
"value_type": "CborString",
"random_hex": "736b060d21fa4e37f17b0b0568285add9d064ba883eb6c35",
"serialized_hex": "a4686469676573744944006672616e646f6d5818736b060d21fa4e37f17b0b0568285add9d064ba883eb6c3571656c656d656e744964656e7469666965726b66616d696c795f6e616d656c656c656d656e7456616c756563446f65"
},
{
"id": "given_name",
"digest_id": 1,
"value": "John",
"value_type": "CborString",
"random_hex": "312e734c986fdf279c7d75130b0da4794d16d44bbb3cfd61",
"serialized_hex": "a4686469676573744944016672616e646f6d5818312e734c986fdf279c7d75130b0da4794d16d44bbb3cfd6171656c656d656e744964656e7469666965726a676976656e5f6e616d656c656c656d656e7456616c7565644a6f686e"
},
{
"id": "birth_date",
"digest_id": 2,
"value": "1986-03-22",
"value_type": "CborString",
"random_hex": "e6864d13d13a9fb1a7fccb729a6330dec975bac5e79fa023",
"serialized_hex": "a4686469676573744944026672616e646f6d5818e6864d13d13a9fb1a7fccb729a6330dec975bac5e79fa02371656c656d656e744964656e7469666965726a62697274685f646174656c656c656d656e7456616c7565d903ec6a313938362d30332d3232"
}
]
},
"matching_digest": {
"org.iso.18013.5.1": [
"family_name",
"given_name",
"birth_date"
]
}
},
"errors": [],
"execution_time": "PT0.003104834S"
},
"mso_mdoc/mso": {
"policy_executed": {
"policy": "mso_mdoc/mso",
"id": "mso_mdoc/mso",
"description": "Verify MSO"
},
"success": true,
"results": {
"signed": "2026-05-18T07:48:27.870905006Z",
"valid_from": "2027-05-18T07:48:27.852289522Z"
},
"errors": [],
"execution_time": "PT0.004245834S"
}
}
},
"presented_raw_data": {
"vpToken": {
"credential_1": [
"o2d2ZXJzaW9uYzEuMGlkb2N1bWVudHOBo2dkb2NUeXBldW9yZy5pc28uMTgwMTMuNS4xLm1ETGxpc3N1ZXJTaWduZWSiam5hbWVTcGFjZXOhcW9yZy5pc28uMTgwMTMuNS4xg9gYWFukaGRpZ2VzdElEAGZyYW5kb21YGHNrBg0h-k438XsLBWgoWt2dBkuog-tsNXFlbGVtZW50SWRlbnRpZmllcmtmYW1pbHlfbmFtZWxlbGVtZW50VmFsdWVjRG9l2BhYW6RoZGlnZXN0SUQBZnJhbmRvbVgYMS5zTJhv3yecfXUTCw2keU0W1Eu7PP1hcWVsZW1lbnRJZGVudGlmaWVyamdpdmVuX25hbWVsZWxlbWVudFZhbHVlZEpvaG7YGFhkpGhkaWdlc3RJRAJmcmFuZG9tWBjmhk0T0Tqfsaf8y3KaYzDeyXW6xeefoCNxZWxlbWVudElkZW50aWZpZXJqYmlydGhfZGF0ZWxlbGVtZW50VmFsdWXZA-xqMTk4Ni0wMy0yMmppc3N1ZXJBdXRohEOhASahGCFZAi8wggIrMIIB0aADAgECAhRED-gfURxpHvIfRtUe91K73mZpljAKBggqhkjOPQQDAjAsMQswCQYDVQQGEwJBVDEdMBsGA1UEAwwUbURMIElzc3VlciBJQUNBIFJvb3QwHhcNMjYwNTE4MDYxOTI2WhcNMjcwODE4MDYxOTI2WjAyMQswCQYDVQQGEwJBVDEjMCEGA1UEAwwabURMIElzc3VlciBEb2N1bWVudCBTaWduZXIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATJ-Zu0Kx1BD_gQM-DQa33yAliTXOQ06kYz1sdgSLj2ZzAQgLIneJJ-8eij99W8M0adPNA-l5W0dhsZpnO-wij5o4HKMIHHMB8GA1UdIwQYMBaAFHKy929J0Lyz3XDMLWSZozeAAujjMB0GA1UdDgQWBBRcZNa5S0U4sUU0Uls5Qu74-EGCfTAOBgNVHQ8BAf8EBAMCB4AwJwYDVR0SBCAwHoYcaHR0cHM6Ly9pYWNhLnRlbmFudDEud2FsdC5pZDAVBgNVHSUBAf8ECzAJBgcogYxdBQECMDUGA1UdHwQuMCwwKqAooCaGJGh0dHBzOi8vY3JsLnRlbmFudDEud2FsdC5pZC9pYWNhLmNybDAKBggqhkjOPQQDAgNIADBFAiAGx-rtONQdU9NSJNW0IMpBEJ1fOnr95HOo4ofthoij1wIhAOt4gpN4I3Efm8kJ4_iIYUNtopa4ggZs7X_fZvBX8K85WQKV2BhZApCmZ3ZlcnNpb25jMS4wb2RpZ2VzdEFsZ29yaXRobWdTSEEtMjU2bHZhbHVlRGlnZXN0c6Fxb3JnLmlzby4xODAxMy41LjGqAFgglIt2JC3rDbrr6azX5GWPMGyGqyD5gTnyASk37pDhZQYBWCCMQ7Ph45igVnLbvCtlSqLPlaMTXSk2kLoy4Mxn1x37kwJYIOjGjCHBlfvZi74lANZXAWt_FDdDf6NU591eYMGbFladA1ggcdl99l7hzcOFDYUeghGX0gDXYYgEN7WyRFrgSSFVvv4EWCCKwt6H37DEK8kJFcs__AiFZzLDc-4GPFKuYYEJzv_X1QVYIMybLDIPi3G7go7Bq4ggn0UYXEpmKMazxXZH25mnl0RQBlggofgVz7C7i4z0Iz8zrEPhNBRMlZcr4EhqQCR2pjo3qsoHWCDaJ4DszfMYhI0HfyOcSgMEgx_K12dx34khKW6ohKRhZghYIPTE7TeLsXVDSX9l3DTPB4zDhzF1WDTmCw3Ecxv1deNJCVggqueG-PUmdP8sJdsQ6XbuByQzAZXoRdwB-7dn0hxtkCdtZGV2aWNlS2V5SW5mb6FpZGV2aWNlS2V5owEBIAYhWCCPFDBFXu0dzdb8mQpWwNkvv8O0_jPI0m1DhivXPv4x3mdkb2NUeXBldW9yZy5pc28uMTgwMTMuNS4xLm1ETGx2YWxpZGl0eUluZm-jZnNpZ25lZMB4HjIwMjYtMDUtMThUMDc6NDg6MjcuODcwOTA1MDA2Wml2YWxpZEZyb23AeB4yMDI2LTA1LTE4VDA3OjQ4OjI3Ljg3MDkwNTAwNlpqdmFsaWRVbnRpbMB4HjIwMjctMDUtMThUMDc6NDg6MjcuODUyMjg5NTIyWlhAmRC5DQjAwaNaozniXwKmOoMscEyNwA65Y31zyoCJpiGiZxs-fk9bcsZnrDSu5ATPVEIKrf_4hjY8ODArK76q2WxkZXZpY2VTaWduZWSiam5hbWVTcGFjZXPYGEGgamRldmljZUF1dGihb2RldmljZVNpZ25hdHVyZYRDoQEnoPZYQGQUayxOBk2Wp7g7UhumPbtFSQ5h1h4jArb60UGZOsFMvG6MXHBBPd4JdOX40auhbmKNmBjinPfw96QN9XfV-Q9mc3RhdHVzAA"
]
},
"state": "da82aed7-ea36-4303-9c4a-3ded03cdedb4"
},
"presented_presentations": {
"credential_1": {
"type": "mso_mdoc",
"format": "mso_mdoc",
"mdoc": {
"credentialData": {
"docType": "org.iso.18013.5.1.mDL",
"org.iso.18013.5.1": {
"family_name": "Doe",
"given_name": "John",
"birth_date": "1986-03-22"
}
},
"signed": "o2d2ZXJzaW9uYzEuMGlkb2N1bWVudHOBo2dkb2NUeXBldW9yZy5pc28uMTgwMTMuNS4xLm1ETGxpc3N1ZXJTaWduZWSiam5hbWVTcGFjZXOhcW9yZy5pc28uMTgwMTMuNS4xg9gYWFukaGRpZ2VzdElEAGZyYW5kb21YGHNrBg0h-k438XsLBWgoWt2dBkuog-tsNXFlbGVtZW50SWRlbnRpZmllcmtmYW1pbHlfbmFtZWxlbGVtZW50VmFsdWVjRG9l2BhYW6RoZGlnZXN0SUQBZnJhbmRvbVgYMS5zTJhv3yecfXUTCw2keU0W1Eu7PP1hcWVsZW1lbnRJZGVudGlmaWVyamdpdmVuX25hbWVsZWxlbWVudFZhbHVlZEpvaG7YGFhkpGhkaWdlc3RJRAJmcmFuZG9tWBjmhk0T0Tqfsaf8y3KaYzDeyXW6xeefoCNxZWxlbWVudElkZW50aWZpZXJqYmlydGhfZGF0ZWxlbGVtZW50VmFsdWXZA-xqMTk4Ni0wMy0yMmppc3N1ZXJBdXRohEOhASahGCFZAi8wggIrMIIB0aADAgECAhRED-gfURxpHvIfRtUe91K73mZpljAKBggqhkjOPQQDAjAsMQswCQYDVQQGEwJBVDEdMBsGA1UEAwwUbURMIElzc3VlciBJQUNBIFJvb3QwHhcNMjYwNTE4MDYxOTI2WhcNMjcwODE4MDYxOTI2WjAyMQswCQYDVQQGEwJBVDEjMCEGA1UEAwwabURMIElzc3VlciBEb2N1bWVudCBTaWduZXIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATJ-Zu0Kx1BD_gQM-DQa33yAliTXOQ06kYz1sdgSLj2ZzAQgLIneJJ-8eij99W8M0adPNA-l5W0dhsZpnO-wij5o4HKMIHHMB8GA1UdIwQYMBaAFHKy929J0Lyz3XDMLWSZozeAAujjMB0GA1UdDgQWBBRcZNa5S0U4sUU0Uls5Qu74-EGCfTAOBgNVHQ8BAf8EBAMCB4AwJwYDVR0SBCAwHoYcaHR0cHM6Ly9pYWNhLnRlbmFudDEud2FsdC5pZDAVBgNVHSUBAf8ECzAJBgcogYxdBQECMDUGA1UdHwQuMCwwKqAooCaGJGh0dHBzOi8vY3JsLnRlbmFudDEud2FsdC5pZC9pYWNhLmNybDAKBggqhkjOPQQDAgNIADBFAiAGx-rtONQdU9NSJNW0IMpBEJ1fOnr95HOo4ofthoij1wIhAOt4gpN4I3Efm8kJ4_iIYUNtopa4ggZs7X_fZvBX8K85WQKV2BhZApCmZ3ZlcnNpb25jMS4wb2RpZ2VzdEFsZ29yaXRobWdTSEEtMjU2bHZhbHVlRGlnZXN0c6Fxb3JnLmlzby4xODAxMy41LjGqAFgglIt2JC3rDbrr6azX5GWPMGyGqyD5gTnyASk37pDhZQYBWCCMQ7Ph45igVnLbvCtlSqLPlaMTXSk2kLoy4Mxn1x37kwJYIOjGjCHBlfvZi74lANZXAWt_FDdDf6NU591eYMGbFladA1ggcdl99l7hzcOFDYUeghGX0gDXYYgEN7WyRFrgSSFVvv4EWCCKwt6H37DEK8kJFcs__AiFZzLDc-4GPFKuYYEJzv_X1QVYIMybLDIPi3G7go7Bq4ggn0UYXEpmKMazxXZH25mnl0RQBlggofgVz7C7i4z0Iz8zrEPhNBRMlZcr4EhqQCR2pjo3qsoHWCDaJ4DszfMYhI0HfyOcSgMEgx_K12dx34khKW6ohKRhZghYIPTE7TeLsXVDSX9l3DTPB4zDhzF1WDTmCw3Ecxv1deNJCVggqueG-PUmdP8sJdsQ6XbuByQzAZXoRdwB-7dn0hxtkCdtZGV2aWNlS2V5SW5mb6FpZGV2aWNlS2V5owEBIAYhWCCPFDBFXu0dzdb8mQpWwNkvv8O0_jPI0m1DhivXPv4x3mdkb2NUeXBldW9yZy5pc28uMTgwMTMuNS4xLm1ETGx2YWxpZGl0eUluZm-jZnNpZ25lZMB4HjIwMjYtMDUtMThUMDc6NDg6MjcuODcwOTA1MDA2Wml2YWxpZEZyb23AeB4yMDI2LTA1LTE4VDA3OjQ4OjI3Ljg3MDkwNTAwNlpqdmFsaWRVbnRpbMB4HjIwMjctMDUtMThUMDc6NDg6MjcuODUyMjg5NTIyWlhAmRC5DQjAwaNaozniXwKmOoMscEyNwA65Y31zyoCJpiGiZxs-fk9bcsZnrDSu5ATPVEIKrf_4hjY8ODArK76q2WxkZXZpY2VTaWduZWSiam5hbWVTcGFjZXPYGEGgamRldmljZUF1dGihb2RldmljZVNpZ25hdHVyZYRDoQEnoPZYQGQUayxOBk2Wp7g7UhumPbtFSQ5h1h4jArb60UGZOsFMvG6MXHBBPd4JdOX40auhbmKNmBjinPfw96QN9XfV-Q9mc3RhdHVzAA",
"docType": "org.iso.18013.5.1.mDL",
"signature": {
"type": "signature-cose",
"signerKey": {
"type": "jwk",
"jwk": {
"kty": "EC",
"crv": "P-256",
"x": "yfmbtCsdQQ_4EDPg0Gt98gJYk1zkNOpGM9bHYEi49mc",
"y": "MBCAsid4kn7x6KP31bwzRp080D6XlbR2Gxmmc77CKPk"
}
},
"x5cList": [
"MIICKzCCAdGgAwIBAgIURA/oH1EcaR7yH0bVHvdSu95maZYwCgYIKoZIzj0EAwIwLDELMAkGA1UEBhMCQVQxHTAbBgNVBAMMFG1ETCBJc3N1ZXIgSUFDQSBSb290MB4XDTI2MDUxODA2MTkyNloXDTI3MDgxODA2MTkyNlowMjELMAkGA1UEBhMCQVQxIzAhBgNVBAMMGm1ETCBJc3N1ZXIgRG9jdW1lbnQgU2lnbmVyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEyfmbtCsdQQ/4EDPg0Gt98gJYk1zkNOpGM9bHYEi49mcwEICyJ3iSfvHoo/fVvDNGnTzQPpeVtHYbGaZzvsIo+aOByjCBxzAfBgNVHSMEGDAWgBRysvdvSdC8s91wzC1kmaM3gALo4zAdBgNVHQ4EFgQUXGTWuUtFOLFFNFJbOULu+PhBgn0wDgYDVR0PAQH/BAQDAgeAMCcGA1UdEgQgMB6GHGh0dHBzOi8vaWFjYS50ZW5hbnQxLndhbHQuaWQwFQYDVR0lAQH/BAswCQYHKIGMXQUBAjA1BgNVHR8ELjAsMCqgKKAmhiRodHRwczovL2NybC50ZW5hbnQxLndhbHQuaWQvaWFjYS5jcmwwCgYIKoZIzj0EAwIDSAAwRQIgBsfq7TjUHVPTUiTVtCDKQRCdXzp6/eRzqOKH7YaIo9cCIQDreIKTeCNxH5vJCeP4iGFDbaKWuIIGbO1/32bwV/CvOQ=="
]
},
"format": "mso_mdoc",
"mso": {
"version": "1.0",
"digestAlgorithm": "SHA-256",
"valueDigests": {
"org.iso.18013.5.1": {
"0": [
-108,
-117,
118,
36,
45,
-21,
13,
-70,
-21,
-23,
-84,
-41,
-28,
101,
-113,
48,
108,
-122,
-85,
32,
-7,
-127,
57,
-14,
1,
41,
55,
-18,
-112,
-31,
101,
6
],
"1": [
-116,
67,
-77,
-31,
-29,
-104,
-96,
86,
114,
-37,
-68,
43,
101,
74,
-94,
-49,
-107,
-93,
19,
93,
41,
54,
-112,
-70,
50,
-32,
-52,
103,
-41,
29,
-5,
-109
],
"2": [
-24,
-58,
-116,
33,
-63,
-107,
-5,
-39,
-117,
-66,
37,
0,
-42,
87,
1,
107,
127,
20,
55,
67,
127,
-93,
84,
-25,
-35,
94,
96,
-63,
-101,
22,
86,
-99
],
"3": [
113,
-39,
125,
-10,
94,
-31,
-51,
-61,
-123,
13,
-123,
30,
-126,
17,
-105,
-46,
0,
-41,
97,
-120,
4,
55,
-75,
-78,
68,
90,
-32,
73,
33,
85,
-66,
-2
],
"4": [
-118,
-62,
-34,
-121,
-33,
-80,
-60,
43,
-55,
9,
21,
-53,
63,
-4,
8,
-123,
103,
50,
-61,
115,
-18,
6,
60,
82,
-82,
97,
-127,
9,
-50,
-1,
-41,
-43
],
"5": [
-52,
-101,
44,
50,
15,
-117,
113,
-69,
-126,
-114,
-63,
-85,
-120,
32,
-97,
69,
24,
92,
74,
102,
40,
-58,
-77,
-59,
118,
71,
-37,
-103,
-89,
-105,
68,
80
],
"6": [
-95,
-8,
21,
-49,
-80,
-69,
-117,
-116,
-12,
35,
63,
51,
-84,
67,
-31,
52,
20,
76,
-107,
-105,
43,
-32,
72,
106,
64,
36,
118,
-90,
58,
55,
-86,
-54
],
"7": [
-38,
39,
-128,
-20,
-51,
-13,
24,
-124,
-115,
7,
127,
35,
-100,
74,
3,
4,
-125,
31,
-54,
-41,
103,
113,
-33,
-119,
33,
41,
110,
-88,
-124,
-92,
97,
102
],
"8": [
-12,
-60,
-19,
55,
-117,
-79,
117,
67,
73,
127,
101,
-36,
52,
-49,
7,
-116,
-61,
-121,
49,
117,
88,
52,
-26,
11,
13,
-60,
115,
27,
-11,
117,
-29,
73
],
"9": [
-86,
-25,
-122,
-8,
-11,
38,
116,
-1,
44,
37,
-37,
16,
-23,
118,
-18,
7,
36,
51,
1,
-107,
-24,
69,
-36,
1,
-5,
-73,
103,
-46,
28,
109,
-112,
39
]
}
},
"deviceKeyInfo": {
"deviceKey": {
"kty": 1,
"crv": 6,
"x": [
-113,
20,
48,
69,
94,
-19,
29,
-51,
-42,
-4,
-103,
10,
86,
-64,
-39,
47,
-65,
-61,
-76,
-2,
51,
-56,
-46,
109,
67,
-122,
43,
-41,
62,
-2,
49,
-34
]
}
},
"docType": "org.iso.18013.5.1.mDL",
"validityInfo": {
"signed": "2026-05-18T07:48:27.870905006Z",
"validFrom": "2026-05-18T07:48:27.870905006Z",
"validUntil": "2027-05-18T07:48:27.852289522Z"
}
}
}
}
},
"presented_credentials": {
"credential_1": [
{
"type": "vc-mdocs",
"credentialData": {
"docType": "org.iso.18013.5.1.mDL",
"org.iso.18013.5.1": {
"family_name": "Doe",
"given_name": "John",
"birth_date": "1986-03-22"
}
},
"signed": "o2d2ZXJzaW9uYzEuMGlkb2N1bWVudHOBo2dkb2NUeXBldW9yZy5pc28uMTgwMTMuNS4xLm1ETGxpc3N1ZXJTaWduZWSiam5hbWVTcGFjZXOhcW9yZy5pc28uMTgwMTMuNS4xg9gYWFukaGRpZ2VzdElEAGZyYW5kb21YGHNrBg0h-k438XsLBWgoWt2dBkuog-tsNXFlbGVtZW50SWRlbnRpZmllcmtmYW1pbHlfbmFtZWxlbGVtZW50VmFsdWVjRG9l2BhYW6RoZGlnZXN0SUQBZnJhbmRvbVgYMS5zTJhv3yecfXUTCw2keU0W1Eu7PP1hcWVsZW1lbnRJZGVudGlmaWVyamdpdmVuX25hbWVsZWxlbWVudFZhbHVlZEpvaG7YGFhkpGhkaWdlc3RJRAJmcmFuZG9tWBjmhk0T0Tqfsaf8y3KaYzDeyXW6xeefoCNxZWxlbWVudElkZW50aWZpZXJqYmlydGhfZGF0ZWxlbGVtZW50VmFsdWXZA-xqMTk4Ni0wMy0yMmppc3N1ZXJBdXRohEOhASahGCFZAi8wggIrMIIB0aADAgECAhRED-gfURxpHvIfRtUe91K73mZpljAKBggqhkjOPQQDAjAsMQswCQYDVQQGEwJBVDEdMBsGA1UEAwwUbURMIElzc3VlciBJQUNBIFJvb3QwHhcNMjYwNTE4MDYxOTI2WhcNMjcwODE4MDYxOTI2WjAyMQswCQYDVQQGEwJBVDEjMCEGA1UEAwwabURMIElzc3VlciBEb2N1bWVudCBTaWduZXIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATJ-Zu0Kx1BD_gQM-DQa33yAliTXOQ06kYz1sdgSLj2ZzAQgLIneJJ-8eij99W8M0adPNA-l5W0dhsZpnO-wij5o4HKMIHHMB8GA1UdIwQYMBaAFHKy929J0Lyz3XDMLWSZozeAAujjMB0GA1UdDgQWBBRcZNa5S0U4sUU0Uls5Qu74-EGCfTAOBgNVHQ8BAf8EBAMCB4AwJwYDVR0SBCAwHoYcaHR0cHM6Ly9pYWNhLnRlbmFudDEud2FsdC5pZDAVBgNVHSUBAf8ECzAJBgcogYxdBQECMDUGA1UdHwQuMCwwKqAooCaGJGh0dHBzOi8vY3JsLnRlbmFudDEud2FsdC5pZC9pYWNhLmNybDAKBggqhkjOPQQDAgNIADBFAiAGx-rtONQdU9NSJNW0IMpBEJ1fOnr95HOo4ofthoij1wIhAOt4gpN4I3Efm8kJ4_iIYUNtopa4ggZs7X_fZvBX8K85WQKV2BhZApCmZ3ZlcnNpb25jMS4wb2RpZ2VzdEFsZ29yaXRobWdTSEEtMjU2bHZhbHVlRGlnZXN0c6Fxb3JnLmlzby4xODAxMy41LjGqAFgglIt2JC3rDbrr6azX5GWPMGyGqyD5gTnyASk37pDhZQYBWCCMQ7Ph45igVnLbvCtlSqLPlaMTXSk2kLoy4Mxn1x37kwJYIOjGjCHBlfvZi74lANZXAWt_FDdDf6NU591eYMGbFladA1ggcdl99l7hzcOFDYUeghGX0gDXYYgEN7WyRFrgSSFVvv4EWCCKwt6H37DEK8kJFcs__AiFZzLDc-4GPFKuYYEJzv_X1QVYIMybLDIPi3G7go7Bq4ggn0UYXEpmKMazxXZH25mnl0RQBlggofgVz7C7i4z0Iz8zrEPhNBRMlZcr4EhqQCR2pjo3qsoHWCDaJ4DszfMYhI0HfyOcSgMEgx_K12dx34khKW6ohKRhZghYIPTE7TeLsXVDSX9l3DTPB4zDhzF1WDTmCw3Ecxv1deNJCVggqueG-PUmdP8sJdsQ6XbuByQzAZXoRdwB-7dn0hxtkCdtZGV2aWNlS2V5SW5mb6FpZGV2aWNlS2V5owEBIAYhWCCPFDBFXu0dzdb8mQpWwNkvv8O0_jPI0m1DhivXPv4x3mdkb2NUeXBldW9yZy5pc28uMTgwMTMuNS4xLm1ETGx2YWxpZGl0eUluZm-jZnNpZ25lZMB4HjIwMjYtMDUtMThUMDc6NDg6MjcuODcwOTA1MDA2Wml2YWxpZEZyb23AeB4yMDI2LTA1LTE4VDA3OjQ4OjI3Ljg3MDkwNTAwNlpqdmFsaWRVbnRpbMB4HjIwMjctMDUtMThUMDc6NDg6MjcuODUyMjg5NTIyWlhAmRC5DQjAwaNaozniXwKmOoMscEyNwA65Y31zyoCJpiGiZxs-fk9bcsZnrDSu5ATPVEIKrf_4hjY8ODArK76q2WxkZXZpY2VTaWduZWSiam5hbWVTcGFjZXPYGEGgamRldmljZUF1dGihb2RldmljZVNpZ25hdHVyZYRDoQEnoPZYQGQUayxOBk2Wp7g7UhumPbtFSQ5h1h4jArb60UGZOsFMvG6MXHBBPd4JdOX40auhbmKNmBjinPfw96QN9XfV-Q9mc3RhdHVzAA",
"docType": "org.iso.18013.5.1.mDL",
"signature": {
"type": "signature-cose",
"signerKey": {
"type": "jwk",
"jwk": {
"kty": "EC",
"crv": "P-256",
"x": "yfmbtCsdQQ_4EDPg0Gt98gJYk1zkNOpGM9bHYEi49mc",
"y": "MBCAsid4kn7x6KP31bwzRp080D6XlbR2Gxmmc77CKPk"
}
},
"x5cList": [
"MIICKzCCAdGgAwIBAgIURA/oH1EcaR7yH0bVHvdSu95maZYwCgYIKoZIzj0EAwIwLDELMAkGA1UEBhMCQVQxHTAbBgNVBAMMFG1ETCBJc3N1ZXIgSUFDQSBSb290MB4XDTI2MDUxODA2MTkyNloXDTI3MDgxODA2MTkyNlowMjELMAkGA1UEBhMCQVQxIzAhBgNVBAMMGm1ETCBJc3N1ZXIgRG9jdW1lbnQgU2lnbmVyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEyfmbtCsdQQ/4EDPg0Gt98gJYk1zkNOpGM9bHYEi49mcwEICyJ3iSfvHoo/fVvDNGnTzQPpeVtHYbGaZzvsIo+aOByjCBxzAfBgNVHSMEGDAWgBRysvdvSdC8s91wzC1kmaM3gALo4zAdBgNVHQ4EFgQUXGTWuUtFOLFFNFJbOULu+PhBgn0wDgYDVR0PAQH/BAQDAgeAMCcGA1UdEgQgMB6GHGh0dHBzOi8vaWFjYS50ZW5hbnQxLndhbHQuaWQwFQYDVR0lAQH/BAswCQYHKIGMXQUBAjA1BgNVHR8ELjAsMCqgKKAmhiRodHRwczovL2NybC50ZW5hbnQxLndhbHQuaWQvaWFjYS5jcmwwCgYIKoZIzj0EAwIDSAAwRQIgBsfq7TjUHVPTUiTVtCDKQRCdXzp6/eRzqOKH7YaIo9cCIQDreIKTeCNxH5vJCeP4iGFDbaKWuIIGbO1/32bwV/CvOQ=="
]
},
"format": "mso_mdoc",
"mso": {
"version": "1.0",
"digestAlgorithm": "SHA-256",
"valueDigests": {
"org.iso.18013.5.1": {
"0": [
-108,
-117,
118,
36,
45,
-21,
13,
-70,
-21,
-23,
-84,
-41,
-28,
101,
-113,
48,
108,
-122,
-85,
32,
-7,
-127,
57,
-14,
1,
41,
55,
-18,
-112,
-31,
101,
6
],
"1": [
-116,
67,
-77,
-31,
-29,
-104,
-96,
86,
114,
-37,
-68,
43,
101,
74,
-94,
-49,
-107,
-93,
19,
93,
41,
54,
-112,
-70,
50,
-32,
-52,
103,
-41,
29,
-5,
-109
],
"2": [
-24,
-58,
-116,
33,
-63,
-107,
-5,
-39,
-117,
-66,
37,
0,
-42,
87,
1,
107,
127,
20,
55,
67,
127,
-93,
84,
-25,
-35,
94,
96,
-63,
-101,
22,
86,
-99
],
"3": [
113,
-39,
125,
-10,
94,
-31,
-51,
-61,
-123,
13,
-123,
30,
-126,
17,
-105,
-46,
0,
-41,
97,
-120,
4,
55,
-75,
-78,
68,
90,
-32,
73,
33,
85,
-66,
-2
],
"4": [
-118,
-62,
-34,
-121,
-33,
-80,
-60,
43,
-55,
9,
21,
-53,
63,
-4,
8,
-123,
103,
50,
-61,
115,
-18,
6,
60,
82,
-82,
97,
-127,
9,
-50,
-1,
-41,
-43
],
"5": [
-52,
-101,
44,
50,
15,
-117,
113,
-69,
-126,
-114,
-63,
-85,
-120,
32,
-97,
69,
24,
92,
74,
102,
40,
-58,
-77,
-59,
118,
71,
-37,
-103,
-89,
-105,
68,
80
],
"6": [
-95,
-8,
21,
-49,
-80,
-69,
-117,
-116,
-12,
35,
63,
51,
-84,
67,
-31,
52,
20,
76,
-107,
-105,
43,
-32,
72,
106,
64,
36,
118,
-90,
58,
55,
-86,
-54
],
"7": [
-38,
39,
-128,
-20,
-51,
-13,
24,
-124,
-115,
7,
127,
35,
-100,
74,
3,
4,
-125,
31,
-54,
-41,
103,
113,
-33,
-119,
33,
41,
110,
-88,
-124,
-92,
97,
102
],
"8": [
-12,
-60,
-19,
55,
-117,
-79,
117,
67,
73,
127,
101,
-36,
52,
-49,
7,
-116,
-61,
-121,
49,
117,
88,
52,
-26,
11,
13,
-60,
115,
27,
-11,
117,
-29,
73
],
"9": [
-86,
-25,
-122,
-8,
-11,
38,
116,
-1,
44,
37,
-37,
16,
-23,
118,
-18,
7,
36,
51,
1,
-107,
-24,
69,
-36,
1,
-5,
-73,
103,
-46,
28,
109,
-112,
39
]
}
},
"deviceKeyInfo": {
"deviceKey": {
"kty": 1,
"crv": 6,
"x": [
-113,
20,
48,
69,
94,
-19,
29,
-51,
-42,
-4,
-103,
10,
86,
-64,
-39,
47,
-65,
-61,
-76,
-2,
51,
-56,
-46,
109,
67,
-122,
43,
-41,
62,
-2,
49,
-34
]
}
},
"docType": "org.iso.18013.5.1.mDL",
"validityInfo": {
"signed": "2026-05-18T07:48:27.870905006Z",
"validFrom": "2026-05-18T07:48:27.870905006Z",
"validUntil": "2027-05-18T07:48:27.852289522Z"
}
}
}
]
}
}
```
**Key Response Fields:**
- **id**: _String_ - The verification session ID
- **setup**: _Object_ - The complete session configuration that was used to create this verification session (the `VerificationSessionSetup` from the create request)
- **creationDate**: _String_ - ISO 8601 timestamp when the session was created
- **expirationDate**: _String_ - ISO 8601 timestamp when the session expires (default: 10 minutes after creation). Session will expire if left unused (no presentation pushed). Once a presentation is received, the session is no longer eligible for expiry.
- **status**: _String_ - The current status of the verification session. Possible values:
- `"UNKNOWN"` — Session ended up in unknown flow (should be avoided)
- `"ACTIVE"` — Session was created and is active (can be used)
- `"UNUSED"` — Session was not used yet, but not expired (can be used)
- `"IN_USE"` — AuthorizationRequest was requested by wallet
- `"VALIDATING_RECEIVED_REQUEST"` — Checking if received presentation will be processed
- `"PROCESSING_FLOW"` — Received presentation is being processed (validation + policy execution)
- `"EXPIRED"` — Verification request expired without being utilized
- `"SUCCESSFUL"` — Verification completed fully successfully (all validation & policies passed)
- `"FAILED"` — Verification was unsuccessful (presentation validation or policies failed)
- **attempted**: _Boolean_ - Whether the wallet has attempted to respond to the verification request
- **reattemptable**: _Boolean_ - Whether the user can retry this verification session if it failed
- **policies**: _Object_ - The policies configured for this session:
- **vp_policies**: _Object_ (optional) - Policies to run on presentations
- **vc_policies**: _Array_ (optional) - Policies to run on credentials
- **specific_vc_policies**: _Object_ (optional) - Policies to run on specific credential IDs
- **policy_results**: _Object_ (optional) - Detailed results for all policies applied to the verification (only present after presentation is received):
- **vp_policies**: _Object_ - Results for presentation-level policies (audience check, nonce check, envelope signature), organized by credential ID. Structure: `{ "credential_id": { "policy_name": { ... } } }`
- **vc_policies**: _Array_ - Results for credential-level policies (signature, expiry, schema, etc.). Each item contains `policy`, `success`, and `result`.
- **specific_vc_policies**: _Object_ - Results for credential-specific policies
- **overallSuccess**: _Boolean_ (computed) - Whether all policies passed
- **redirects**: _Object_ (optional) - Success and error redirect URIs if configured
**Note:**
Instead of polling the session status, you can use **callbacks** or **Server Sent Events (SSE)** to be notified when the verification completes. See [Callbacks & SSE](https://docs.walt.id/community-stack/verifier2/callback-and-sse.md) for details.
---
## Understanding Flow Types and Core Concepts
Now that you've seen verification in action, let's understand the underlying structure.
### Verification Session Structure
A `VerificationSession` defines the full lifecycle: authorization request → user presentation → policy validation → final result.
Every verification request follows this structure:
```json
{
"flow_type": "cross_device | same_device | dc_api",
"core_flow": { /* common to all flows */ },
"url_config": { /* specific to cross_device and same_device */ },
"redirects": { /* optional */ }
}
```
**Why this structure?**
- **flow_type** determines how the credential request is delivered (QR code vs deep link vs browser API)
- **core_flow** contains the credential query and policies (same regardless of delivery method)
- **url_config** and **redirects** are flow-specific attributes
This separation makes it easy to switch between flows without changing your credential requirements.
### Flow Type Comparison
| Flow Type | Use Case | Required Attributes | User Experience |
|-----------|----------|---------------------|-----------------|
| `cross_device` | User scans QR code from desktop | None beyond core flow | Desktop shows QR → User scans with phone → Phone wallet opens |
| `same_device` | User clicks link on mobile | `redirects` (success/error URLs) | Browser redirects to wallet → Wallet opens → User returns to browser |
| `dc_api` | Browser-native credential exchange | `expectedOrigins` (array of allowed origins) | Browser shows native credential picker → User selects → Instant return |
---
## Configuration Defaults
Many session parameters can be configured globally in `verifier-service.conf` and overridden per-request when needed.
| Parameter | Config File Location | Request Override Path | When to Override |
|-----------|---------------------|----------------------|------------------|
| `urlPrefix` | `verifier-service.conf` → `urlPrefix` | `url_config.url_prefix` | Testing with ngrok, different environments per session |
| `urlHost` | `verifier-service.conf` → `urlHost` | `url_config.url_host` | Custom authorization endpoints, conformance testing |
| `clientId` | `verifier-service.conf` → `clientId` | `core_flow.clientId` | Different verifier identities per use case |
| `clientMetadata` | `verifier-service.conf` → `clientMetadata` | `core_flow.client_metadata` | Session-specific branding, multi-tenant scenarios |
| Signing key | `verifier-service.conf` → `key` | `core_flow.key` | Different signing keys per session (advanced) |
| Certificate chain | `verifier-service.conf` → `x5c` | `core_flow.x5c` | Different certificate chains per session (x509 flows) |
**Note:**
**Best Practice:** Set defaults in `verifier-service.conf` for your primary use case, and override per-request only when necessary. This keeps your API calls clean and reduces duplication.
See [Verifier Service Configuration](https://docs.walt.id/community-stack/verifier2/configurations/config-files/verifier-service.md) for detailed configuration documentation.
---
## Advanced Options
### Requesting Specific Claims
Mobile driver's licenses support selective disclosure. You can request specific claims using the `claims` array with namespace-qualified paths:
```json
{
"flow_type": "cross_device",
"core_flow": {
"dcql_query": {
"credentials": [
{
"id": "credential_1",
"format": "mso_mdoc",
"meta": {
"doctype_value": "org.iso.18013.5.1.mDL"
},
"claims": [
{
"path": ["org.iso.18013.5.1", "family_name"]
},
{
"path": ["org.iso.18013.5.1", "given_name"]
},
{
"path": ["org.iso.18013.5.1", "birth_date"]
},
{
"path": ["org.iso.18013.5.1", "portrait"]
}
]
}
]
}
}
}
```
This requests only specific claims from the mDL. The user's wallet will only disclose these specific data elements.
### Claim Values
You can also request claims with specific values (for filtering):
```json
{
"claims": [
{
"path": ["org.iso.18013.5.1", "issuing_country"],
"values": ["US", "CA"]
}
]
}
```
This requests only mDLs issued by US or Canadian authorities.
### Custom Policies
Beyond signature verification, you can apply additional policies:
```json
"policies": {
"vc_policies": [
{ "policy": "signature" },
{ "policy": "expired" },
{ "policy": "not-before" }
]
}
```
See [Policies](https://docs.walt.id/community-stack/verifier2/policies/overview.md) for the complete list and configuration options.
### VICAL Trust Validation
For mDL credentials, you can apply the VICAL (Verifiable Issuer Certificate Authority List) policy for trust validation:
```json
"policies": {
"vc_policies": [
{
"policy": "signature"
},
{
"policy": "vical",
"vical": "",
"enableDocumentTypeValidation": false,
"enableTrustedChainRoot": false,
"enableSystemTrustAnchors": false,
"enableRevocation": false
}
]
}
```
See the [VICAL policy documentation](https://docs.walt.id/community-stack/verifier2/policies/available-policies.md#vical) for detailed parameter descriptions and configuration examples.
### Transaction Data Authorization
Verifier2 supports OpenID4VP `transaction_data` to bind credential presentations to specific transactions (e.g., payment authorization, account access).
For mDL credentials, the wallet embeds transaction data hashes in the DeviceSigned namespaces, and the verifier evaluates the `mso_mdoc/transaction-data-hash-check` policy automatically to validate the binding.
#### Example Request
```json
{
"flow_type": "cross_device",
"core_flow": {
"dcql_query": {
"credentials": [
{
"id": "mdl",
"format": "mso_mdoc",
"meta": {
"doctype_value": "org.iso.18013.5.1.mDL"
},
"claims": [
{
"path": ["org.iso.18013.5.1", "family_name"]
},
{
"path": ["org.iso.18013.5.1", "given_name"]
}
]
}
]
}
},
"openid": {
"transactionData": [
{
"type": "org.waltid.transaction-data.payment-authorization",
"credential_ids": ["mdl"],
"require_cryptographic_holder_binding": true,
"transaction_data_hashes_alg": ["sha-256"],
"amount": "42.00",
"currency": "EUR",
"payee": "ACME Corp"
}
]
}
}
```
#### Parameters
- **type**: _String_ (required) - The transaction data type identifier. Must match a profile registered via the [`transaction-data-profiles` configuration](https://docs.walt.id/community-stack/verifier2/configurations/config-files/transaction-data-profiles.md). Common types:
- `"org.waltid.transaction-data.payment-authorization"` — For payment transactions (fields: `amount`, `currency`, `payee`)
- `"org.waltid.transaction-data.account-access"` — For account access requests (fields: `account_identifier`, `access_scope`)
- **credential_ids**: _Array of Strings_ (required) - References one or more credential IDs from your DCQL query (`credentials[].id`). The wallet will bind this transaction data to presentations of these credentials. For mDL credentials, use the credential ID you assigned (e.g., `"mdl"`).
- **require_cryptographic_holder_binding**: _Boolean_ (optional) - If provided, it must be set to `true`. Transaction data requires cryptographic holder binding (KB-JWT for SD-JWT VC, DeviceAuth for mDL). Requests with `false` are rejected per [OpenID4VP §B.3.3](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#appendix-B.3.3).
- **transaction_data_hashes_alg**: _Array of Strings_ (optional) — Hash algorithms the wallet uses to bind transaction data. e.g. `["sha-256"]`. If omitted "sha-256" is used.
**Type-Specific Parameters:**
For `"org.waltid.transaction-data.payment-authorization"`:
- **amount**: _String_ (required) - Payment amount (e.g., `"42.00"`)
- **currency**: _String_ (required) - Currency code (e.g., `"EUR"`, `"USD"`)
- **payee**: _String_ (required) - Payee identifier (e.g., `"ACME Corp"`)
For `"org.waltid.transaction-data.account-access"`:
- **account_identifier**: _String_ (required) - Account identifier (e.g., account number, user ID)
- **access_scope**: _String_ (required) - Scope of access being requested (e.g., `"read"`, `"write"`, `"admin"`)
**Note:**
**Encoding Behavior:** You pass transaction data entries as plain JSON objects in your verification request. The verifier service automatically base64url-encodes each entry before placing it in the authorization request sent to the wallet. Do not base64-encode the entries yourself.
#### Constraints
- `transaction_data` requires a `dcql_query`. Requests without one are rejected with `invalid_request`.
- Each entry's `credential_ids` must reference DCQL credential query ids declared in the same request.
- Each referenced credential query must use a transaction-data-capable format: `dc+sd-jwt` or `mso_mdoc`.
- Transaction data with `require_cryptographic_holder_binding: false` is rejected. See [OpenID4VP §B.3.3](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#appendix-B.3.3).
- Transaction-data types are configurable via profiles (e.g. `payment-authorization`, `account-access`). Custom types can be added through the [`transaction-data-profiles` configuration](https://docs.walt.id/community-stack/verifier2/configurations/config-files/transaction-data-profiles.md), and the active profiles are exposed via `GET /transaction-data-profiles`.
- Available on `cross_device`, `same_device`, and `dc_api` (Annex D) flows via the `openid.transactionData` field.
**Note:**
**Automatic Policy Enforcement:** When `transactionData` is present, the verifier automatically adds the appropriate transaction-data hash-check policy (`dc+sd-jwt/transaction-data-hash-check` for SD-JWT, `mso_mdoc/transaction-data-hash-check` for mDL) to ensure the wallet properly binds the transaction data. This happens even if you specify custom `vp_policies`. Your custom policies are preserved; the hash-check policy is added on top.
#### Discovery & Configuration
Transaction data types are configurable via profiles. To see which transaction data types your verifier instance supports:
```bash
curl -X 'GET' \
'http://localhost:7004/transaction-data-profiles' \
-H 'accept: application/json'
```
Custom transaction data types can be added through the [`transaction-data-profiles` configuration](https://docs.walt.id/community-stack/verifier2/configurations/config-files/transaction-data-profiles.md).
### Signed Requests
Sign the authorization request JWT using a verifier signing key. This is required for:
- **HAIP profile** compliance
- **DC API Annex C** (ISO 18013-7) flows
Enable by setting `signedRequest: true` in the request body.
**Key Configuration Required:** Signed requests require a signing key (`key`) to be configured. You can either:
- Set the `key` parameter in the [`verifier-service.conf`](https://docs.walt.id/community-stack/verifier2/configurations/config-files/verifier-service.md#key-optional) as a service-wide default
- Override or provide the `key` directly in the verification request body
**Certificate Chain (`x5c`) Required for:**
- X.509-based client authentication when using `x509_san_dns:` or `x509_hash:` as the `clientId` format
- Signed Annex C (ISO 18013-7) requests
The `x5c` parameter can be configured the same way as `key` — as a service-wide default or per-session override.
### Encrypted Responses
The wallet can encrypt the VP token response. Enable by setting `encryptedResponse: true` in the request body.
**Key Configuration Required:** Encrypted responses require a key (`key`) for decrypting the VP token response. Configure it via [`verifier-service.conf`](https://docs.walt.id/community-stack/verifier2/configurations/config-files/verifier-service.md#key-optional) or provide it in the verification request body.
---
## Next Steps
- **Apply custom policies** — See [Policies](https://docs.walt.id/community-stack/verifier2/policies/overview.md) for validation rules beyond signature verification
- **Set up notifications** — Use [Callbacks & SSE](https://docs.walt.id/community-stack/verifier2/callback-and-sse.md) instead of polling for session status
- **Verify other credential types** — Try [W3C VC](https://docs.walt.id/community-stack/verifier2/credential-verification/vc-oid4vp.md) or [SD-JWT VC](https://docs.walt.id/community-stack/verifier2/credential-verification/sd-jwt-vc-oid4vp.md)
- **Use DC API** — Try the [Digital Credentials API](https://docs.walt.id/community-stack/verifier2/credential-verification/dc-api.md) for browser-native credential exchange
--------------------------------------------------------------------------------
PAGE: Verifying Verifiable Credentials via Digital Credentials API
URL: https://docs.walt.id/community-stack/verifier2/credential-verification/dc-api
RAW: https://docs.walt.id/community-stack/verifier2/credential-verification/dc-api.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Verifying Verifiable Credentials via Digital Credentials API
This guide shows you how to verify credentials using the browser-native Digital Credentials API (DC API). The DC API enables secure, native credential sharing directly through the user's browser or operating system, providing a seamless user experience without redirects or QR codes.
We recommend checking out our [Digital Credential Button project](https://github.com/walt-id/waltid-identity/tree/main/waltid-applications/waltid-digital-credentials), which provides a reference for how to invoke the Browser-based DC API. It contains up to date tutorials for setting up and end to end demo utilizing the DC API. This guide will just focus on how to configure the Verifier API for this use case.
## Prerequisites
Before you begin, ensure you have:
- **Verifier2 API running** — Follow the [Getting Started](https://docs.walt.id/community-stack/verifier2/getting-started.md) or [Setup](https://docs.walt.id/community-stack/verifier2/setup.md) guide
- **DC API-compatible browser** — Chrome 121+, Safari 18+, or Edge 121+ (desktop or mobile)
- **HTTPS enabled** — The DC API only works over secure connections (required for production; localhost is allowed for development)
- **Test credentials** — Use the [walt.id Wallet](https://docs.walt.id/community-stack/wallet/getting-started.md) or any DC API-compatible wallet for testing
- **X.509 certificate and key** (for signed requests) — Required for HAIP profile and DC API using ISO 18013-7 flows
**Note:**
**Local Development:** When running locally via Docker, the API is available at `http://localhost:7004` by default, whether standalone or via Docker Compose. The DC API will work with localhost URLs during development.
## Understanding DC API Protocols
The Verifier2 API supports two DC API protocols:
- DC API using ISO 18013-7 [using `flow_type = "dc_api_18013_7"`]
- DC API using OpenID4VP [using `flow_type = "dc_api_openid4vp"`]
For each of these, we support both same device and cross device flows. You can find many examples in the Digital Credential Button project as well as the swagger documentation for the Verifier API.
### Step 1: Preparing the Verification Session
This example requests an mDL credential using DC API with ISO 18013-7 and signed requests. The user will see a native browser credential picker to select and share their credential.
**Option: CURL**
Endpoint: `/verification-session/create` | [API Reference](https://verifier2.portal.test.waltid.cloud/swagger/index.html#/Verification%20Session%20Management/post_verification_session_create)
**Example Request**
```bash
curl -X 'POST' \
'http://localhost:7004/verification-session/create' \
-H 'accept: */*' \
-H 'Content-Type: application/json' \
-d '{
"flow_type": "dc_api_18013_7",
"core_flow": {
"requestedElements": {
"org.iso.18013.5.1.mDL": {
"org.iso.18013.5.1": [
"family_name",
"given_name",
"age_over_21"
]
}
},
"signed_request": true,
"encrypted_response": true,
"clientId": "x509_hash:abc-xyz-base64url-sha256-hash-of-der-x509-leaf",
"key": {
"type": "jwk",
"jwk": {
"kty": "EC",
"d": "AEb4k1BeTR9xt2NxYZggdzkFLLUkhyyWvyUOq3qSiwA",
"crv": "P-256",
"kid": "_nd-T2YRYLSmuKkJZlRI641zrCIJLTpiHeqMwXuvdug",
"x": "G_TgBc0BkmMipiQ_6gkamIn3mmp7hcTrZuyrLTmknP0",
"y": "VkRMZdXYXSMff5AJLrnHiN0x5MV6u_8vrAcytGUe4z4"
}
},
"x5c": [
"BASE64_DER_ENCODED_LEAF_CERT"
]
},
"expectedOrigins": [
"http://localhost:7004"
]
}'
```
**Note:**
**`x5c` is required whenever `signed_request: true` is used for `dc_api_18013_7`.** Without it, session creation fails with `x5c is required for signed Annex C requests`. It must be an ordered chain in DER-encoded, Base64-encoded format where the first certificate corresponds to `key`, and its SHA-256 hash is what `clientId`'s `x509_hash:` value must match.
**Body Parameters**
- **flow_type**: _String_ (required) - Specifies how the credential request will be delivered to the holder's wallet. Options:
- `"cross_device"` — User scans a QR code on a different device (e.g. desktop browser → phone wallet)
- `"same_device"` — User clicks a deep link on the same device (e.g. phone browser → wallet app on same phone)
- `"dc_api_openid4vp"` — Browser/OS-native Digital Credentials API (Chrome, Safari, iOS/Android native) using OpenID4VP.
- `"dc_api_18013_7"` — Browser/OS-native Digital Credentials API (Chrome, Safari, iOS/Android native) using ISO 18013-7.
- **core_flow**: _Object_ (required) - Defines what credentials to request and which policies to apply. Contains:
- **dcql_query**: _Object_ (required for `dc_api_openid4vp`) - Specifies the credential type, format, and claims to request using the [DCQL query language](https://docs.walt.id/concepts/data-exchange-protocols/dcql.md). You can also use the DCQL builder [here](https://dcql.walt.id/).
- **credentials**: _Array_ - List of credential queries. Each credential query contains:
- **id**: _String_ - Unique identifier for this credential in the request (used in policy results)
- **format**: _String_ - Credential format. For mDL, use `"mso_mdoc"` (mobile security object document).
- **meta.doctype_value**: _String_ - The ISO mDL document type. For mobile driver's licenses, use `"org.iso.18013.5.1.mDL"`.
- **claims**: _Array_ (optional) - Specific claims to request from the credential. Each claim has a `path` array with namespace and claim name (e.g. `["org.iso.18013.5.1", "family_name"]`). If omitted, all claims that are non-selectively disclosable are provided by the wallet.
- **requestedElements**: _Object_ (required for `dc_api_18013_7`) - ISO 18013-7 requested elements, keyed by document type, then namespace, then element names. This is an alternative to `dcql_query`; do not provide both. OpenID4VP flows (`cross_device`, `same_device`, and `dc_api_openid4vp`) must use `dcql_query` and do not accept `requestedElements`.
- **policies**: _Object_ (optional) - Validation rules applied to the received credential. Defaults to signature verification only if omitted.
- **vc_policies**: _Array_ - Policies applied to individual credentials. Each policy has a `policy` field specifying the policy name. Available policies: `signature`, `expired`, `not-before`, `revoked-status-list`, `schema`, and more. See [Policies](https://docs.walt.id/community-stack/verifier2/policies/overview.md) for the complete list.
- **vp_policies**: _Array_ (optional) - Policies applied to the entire presentation (not shown in this example)
- **signed_request** - Whether the request should be signed. Required for `dc_api_18013_7`.
- **encrypted_response** - Whether the response should be encrypted.
- **clientId**: The client ID to be used for the verification as per the HAIP specification. For `dc_api_18013_7`, use `x509_hash:`, matching the leaf certificate in `x5c`.
- **key**: The key used by the wallet to encrypt the response to the verifier (optional based on if you are using encrypted_response).
- **x5c**: _Array of Strings_ (required when `signed_request: true`) - X.509 certificate chain, ordered DER-encoded and Base64-encoded, first entry corresponding to `key`. Without it, session creation fails with `x5c is required for signed Annex C requests`.
- **expectedOrigins**: _Array_ (required for DC API flows) - The expected browser origins for DC API verification. For `dc_api_18013_7`, provide exactly one origin; that value is used for the ISO 18013-7 session transcript.
**Note:**
Many parameters have defaults configured in `verifier-service.conf`. Request-level parameters override these defaults for that specific session only. See [Configuration Defaults](#configuration-defaults) below.
**Response**
**Note:**
**`dc_api_18013_7` does not return `bootstrapAuthorizationRequestUrl`/`fullAuthorizationRequestUrl`.** Unlike `cross_device`, `same_device`, and `dc_api_openid4vp`, this flow returns the browser-native DC API request payload directly in `data` — there's no authorization URL to build a QR code or deep link from.
```json
{
"sessionId": "ff4f0c86-56ce-4e4a-a137-dd5569049ff3",
"data": {
"protocol": "org-iso-mdoc",
"data": {
"deviceRequest": "omd2ZXJzaW9uYzEuMGtkb2NSZXF1ZXN0c4GhbGl0ZW1zUmVxdWVzdNgY...",
"encryptionInfo": "gmVkY2FwaaJlbm9uY2VYJDgwNTRmMjU5LTcxMDEtNGZhNS05MzM2LTM2Mjk5OWYxN2VjYnJyZWNpcGllbnRQdWJsaWNLZXmk..."
}
}
}
```
You will receive a response containing the following fields:
- **sessionId**: _String_ - The unique ID of this verification session. Use this to query the session status.
- **data.protocol**: _String_ - The DC API protocol identifier to pass to the browser's Digital Credentials API. For `dc_api_18013_7`, always `"org-iso-mdoc"`.
- **data.data.deviceRequest**: _String_ - Base64url-encoded CBOR ISO 18013-7 `DeviceRequest`, to be passed as-is into the browser's `navigator.credentials.get()` call.
- **data.data.encryptionInfo**: _String_ - Base64url-encoded CBOR encryption info for the wallet's response, to be passed as-is alongside `deviceRequest`.
This same `data` object can also be re-fetched independently via [Step 2](#step-2-generating-the-dc-api-request) using the `sessionId`.
### Step 2: Generating the DC API Request
For the next step, take the sessionId and make the following request to get the DC API request URL.
**Option: CURL**
```bash
curl -X 'GET' \
'http://localhost:7004/verification-session/{sessionId}/request' \
-H 'accept: application/json'
```
This returns the same `protocol`/`data` payload already included in Step 1's response, to pass into the browser's Digital Credentials API. We recommend checking out our [Digital Credential Button project](https://github.com/walt-id/waltid-identity/tree/main/waltid-applications/waltid-digital-credentials), which provides a reference for how to invoke the Browser-based DC API.
```json
{
"protocol": "org-iso-mdoc",
"data": {
"deviceRequest": "omd2ZXJzaW9uYzEuMGtkb2NSZXF1ZXN0c4GhbGl0ZW1zUmVxdWVzdNgY...",
"encryptionInfo": "gmVkY2FwaaJlbm9uY2VYJDgwNTRmMjU5LTcxMDEtNGZhNS05MzM2LTM2Mjk5OWYxN2VjYnJyZWNpcGllbnRQdWJsaWNLZXmk..."
}
}
```
### Step 3: Handling the response
Once the wallet presents the credential, you will receive a response from the DC API containing the verifiable presentation like so from the wallet:
```json
{
"protocol": "org-iso-mdoc",
"data": {
"response": "o2d2ZXJzaW9uYzEuMGlkb2N1bWVudH..."
}
}
```
- **protocol**: _String_ - Must match the `protocol` value from Step 1/2 (`"org-iso-mdoc"` for `dc_api_18013_7`).
- **data.response**: _String_ - The base64url-encoded CBOR ISO 18013-7 `DeviceResponse` returned by the browser's Digital Credentials API.
Pass this to the following verifier endpoint to verify the credential and run any policies you have configured.
**Option: CURL**
```bash
curl -X 'POST' \
'http://localhost:7004/verification-session/{sessionId}/response' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"protocol": "org-iso-mdoc",
"data": {
"response": "o2d2ZXJzaW9uYzEuMGlkb2N1bWVudH..."
}
}'
```
A missing `data.response` fails with `Missing 'response' attribute in Annex C JSON response from wallet`.
## Validating the Verification Session
You can query the verification session to check if the user has presented their credential and whether validation succeeded.
**Option: CURL**
**Endpoint:** `GET /verification-session/{sessionId}/info` | [API Reference](https://verifier2.portal.test.waltid.cloud/swagger/index.html#/Verification%20Session%20Management/get_verification_session__sessionId__info)
##### Example Request
```bash
curl -X 'GET' \
'http://localhost:7004/verification-session/{sessionId}/info' \
-H 'accept: application/json'
```
**Path Parameters**
- **sessionId**: _String_ (required) - The unique ID of the verification session returned when you created the session. Use this to query the current status and results.
---
##### Example Response
The response contains the complete session information including setup, status, policy results, and presented credentials.
```json
{
"id": "4a803c4b-1dc3-4210-806d-3fb52f1780c8",
"setup":{
"flow_type": "dc_api_18013_7",
"core_flow": {
"requestedElements": {
"org.iso.18013.5.1.mDL": {
"org.iso.18013.5.1": [
"family_name",
"given_name",
"age_over_21"
]
}
},
"signed_request": true,
"encrypted_response": true,
"clientId": "x509_hash:abc-xyz-base64url-sha256-hash-of-der-x509-leaf",
"key": {
"type": "jwk",
"jwk": {
"kty": "EC",
"d": "AEb4k1BeTR9xt2NxYZggdzkFLLUkhyyWvyUOq3qSiwA",
"crv": "P-256",
"kid": "_nd-T2YRYLSmuKkJZlRI641zrCIJLTpiHeqMwXuvdug",
"x": "G_TgBc0BkmMipiQ_6gkamIn3mmp7hcTrZuyrLTmknP0",
"y": "VkRMZdXYXSMff5AJLrnHiN0x5MV6u_8vrAcytGUe4z4"
}
}
},
"expectedOrigins": [
"http://localhost:7004"
]
},
"creationDate": "2026-05-18T07:48:39.335523256Z",
"expirationDate": "2026-05-18T07:53:39.335523256Z",
"retentionDate": "2036-05-18T07:48:39.335523256Z",
"status": "SUCCESSFUL",
"attempted": true,
"reattemptable": true,
"bootstrapAuthorizationRequest": {
"client_id": "verifier2",
"request_uri": "https://575e-2a02-8388-17c1-5580-b97f-5031-3eee-c8a4.ngrok-free.app/verification-session/4a803c4b-1dc3-4210-806d-3fb52f1780c8/request"
},
"bootstrapAuthorizationRequestUrl": "openid4vp://authorize?client_id=verifier2&request_uri=https%3A%2F%2F575e-2a02-8388-17c1-5580-b97f-5031-3eee-c8a4.ngrok-free.app%2Fverification-session%2F4a803c4b-1dc3-4210-806d-3fb52f1780c8%2Frequest",
"authorizationRequest": {
"response_type": "vp_token",
"client_id": "verifier2",
"state": "da82aed7-ea36-4303-9c4a-3ded03cdedb4",
"response_mode": "direct_post",
"nonce": "74b0efc4-331c-488f-9489-7ce72b1c698f",
"response_uri": "https://575e-2a02-8388-17c1-5580-b97f-5031-3eee-c8a4.ngrok-free.app/verification-session/4a803c4b-1dc3-4210-806d-3fb52f1780c8/response",
"dcql_query": {
"credentials": [
{
"id": "credential_1",
"format": "mso_mdoc",
"multiple": false,
"meta": {
"doctype_value": "org.iso.18013.5.1.mDL",
"format": "mso_mdoc"
},
"require_cryptographic_holder_binding": true,
"claims": [
{
"path": [
"org.iso.18013.5.1",
"family_name"
]
},
{
"path": [
"org.iso.18013.5.1",
"given_name"
]
},
{
"path": [
"org.iso.18013.5.1",
"birth_date"
]
}
]
}
]
},
"client_metadata": {
"vp_formats_supported": {
"mso_mdoc": {
"issuerauth_alg_values": [
-7,
-9,
-50
],
"deviceauth_alg_values": [
-7,
-9,
-50,
-65537
]
}
},
"client_name": "Verifier2",
"logo_uri": "https://images.squarespace-cdn.com/content",
"tos_uri": "https://example.com/terms",
"policy_uri": "https://example.com/privacy",
"client_uri": "https://verifier.example.com",
"client_name#fr-FR": "Vérificateur2",
"logo_uri#fr-FR": "https://example.com/logo-fr.png",
"tos_uri#fr-FR": "https://example.com/terms-fr",
"policy_uri#fr-FR": "https://example.com/privacy-fr",
"client_uri#fr-FR": "https://verifier.example.com/fr"
}
},
"authorizationRequestUrl": "openid4vp://authorize?response_type=vp_token&client_id=verifier2&state=da82aed7-ea36-4303-9c4a-3ded03cdedb4&response_mode=direct_post&nonce=74b0efc4-331c-488f-9489-7ce72b1c698f&response_uri=https%3A%2F%2F575e-2a02-8388-17c1-5580-b97f-5031-3eee-c8a4.ngrok-free.app%2Fverification-session%2F4a803c4b-1dc3-4210-806d-3fb52f1780c8%2Fresponse&dcql_query=%7B%22credentials%22%3A%5B%7B%22id%22%3A%22credential_1%22%2C%22format%22%3A%22mso_mdoc%22%2C%22meta%22%3A%7B%22doctype_value%22%3A%22org.iso.18013.5.1.mDL%22%7D%2C%22claims%22%3A%5B%7B%22path%22%3A%5B%22org.iso.18013.5.1%22%2C%22family_name%22%5D%7D%2C%7B%22path%22%3A%5B%22org.iso.18013.5.1%22%2C%22given_name%22%5D%7D%2C%7B%22path%22%3A%5B%22org.iso.18013.5.1%22%2C%22birth_date%22%5D%7D%5D%7D%5D%7D&client_metadata=%7B%22vp_formats_supported%22%3A%7B%22mso_mdoc%22%3A%7B%22issuerauth_alg_values%22%3A%5B-7%2C-9%2C-50%5D%2C%22deviceauth_alg_values%22%3A%5B-7%2C-9%2C-50%2C-65537%5D%7D%7D%2C%22client_name%22%3A%22Verifier2%22%2C%22logo_uri%22%3A%22https%3A%2F%2Fimages.squarespace-cdn.com%2Fcontent%22%2C%22tos_uri%22%3A%22https%3A%2F%2Fexample.com%2Fterms%22%2C%22policy_uri%22%3A%22https%3A%2F%2Fexample.com%2Fprivacy%22%2C%22client_uri%22%3A%22https%3A%2F%2Fverifier.example.com%22%2C%22client_name%23fr-FR%22%3A%22V%C3%A9rificateur2%22%2C%22logo_uri%23fr-FR%22%3A%22https%3A%2F%2Fexample.com%2Flogo-fr.png%22%2C%22tos_uri%23fr-FR%22%3A%22https%3A%2F%2Fexample.com%2Fterms-fr%22%2C%22policy_uri%23fr-FR%22%3A%22https%3A%2F%2Fexample.com%2Fprivacy-fr%22%2C%22client_uri%23fr-FR%22%3A%22https%3A%2F%2Fverifier.example.com%2Ffr%22%7D",
"requestMode": "REQUEST_URI",
"policies": {
"vp_policies": {
"jwt_vc_json": [
"jwt_vc_json/audience-check",
"jwt_vc_json/nonce-check",
"jwt_vc_json/envelope_signature"
],
"dc+sd-jwt": [
"dc+sd-jwt/audience-check",
"dc+sd-jwt/kb-jwt_signature",
"dc+sd-jwt/nonce-check",
"dc+sd-jwt/sd_hash-check"
],
"mso_mdoc": [
"mso_mdoc/device-auth",
"mso_mdoc/device_key_auth",
"mso_mdoc/issuer_auth",
"mso_mdoc/issuer_signed_integrity",
"mso_mdoc/mso"
]
},
"vc_policies": [
{
"policy": "signature",
"id": "signature"
}
]
},
"policy_results": {
"vp_policies": {
"credential_1": {
"mso_mdoc/device-auth": {
"policy_executed": {
"policy": "mso_mdoc/device-auth",
"id": "mso_mdoc/device-auth",
"description": "Verify device authentication"
},
"success": true,
"results": {
"device_public_jwk": {
"kty": "OKP",
"crv": "Ed25519",
"x": "jxQwRV7tHc3W_JkKVsDZL7_DtP4zyNJtQ4Yr1z7-Md4"
},
"device_auth_bytes_hex": "d8185868847444657669636541757468656e7469636174696f6e83f6f682714f70656e494434565048616e646f7665725820bdb670da65dae163a8fe31c591c82da53c6da1b38232c8f4d3657405e4220a43756f72672e69736f2e31383031332e352e312e6d444cd81841a0"
},
"errors": [],
"execution_time": "PT0.002658542S"
},
"mso_mdoc/device_key_auth": {
"policy_executed": {
"policy": "mso_mdoc/device_key_auth",
"id": "mso_mdoc/device_key_auth",
"description": "Verify holder-verified data"
},
"success": true,
"results": {
"empty_device_signed_namespaces": true
},
"errors": [],
"execution_time": "PT0.000800334S"
},
"mso_mdoc/issuer_auth": {
"policy_executed": {
"policy": "mso_mdoc/issuer_auth",
"id": "mso_mdoc/issuer_auth",
"description": "Verify issuer authentication"
},
"success": true,
"results": {
"certificate_chain": [
"3082022b308201d1a0030201020214440fe81f511c691ef21f46d51ef752bbde666996300a06082a8648ce3d040302302c310b3009060355040613024154311d301b06035504030c146d444c20497373756572204941434120526f6f74301e170d3236303531383036313932365a170d3237303831383036313932365a3032310b30090603550406130241543123302106035504030c1a6d444c2049737375657220446f63756d656e74205369676e65723059301306072a8648ce3d020106082a8648ce3d03010703420004c9f99bb42b1d410ff81033e0d06b7df20258935ce434ea4633d6c76048b8f667301080b22778927ef1e8a3f7d5bc33469d3cd03e9795b4761b19a673bec228f9a381ca3081c7301f0603551d2304183016801472b2f76f49d0bcb3dd70cc2d6499a3378002e8e3301d0603551d0e041604145c64d6b94b4538b14534525b3942eef8f841827d300e0603551d0f0101ff04040302078030270603551d120420301e861c68747470733a2f2f696163612e74656e616e74312e77616c742e696430150603551d250101ff040b3009060728818c5d05010230350603551d1f042e302c302aa028a026862468747470733a2f2f63726c2e74656e616e74312e77616c742e69642f696163612e63726c300a06082a8648ce3d0403020348003045022006c7eaed38d41d53d35224d5b420ca41109d5f3a7afde473a8e287ed8688a3d7022100eb7882937823711f9bc909e3f88861436da296b882066ced7fdf66f057f0af39"
],
"signer_pem": "-----BEGIN CERTIFICATE-----\nMIICKzCCAdGgAwIBAgIURA/oH1EcaR7yH0bVHvdSu95maZYwCgYIKoZIzj0EAwIw\r\nLDELMAkGA1UEBhMCQVQxHTAbBgNVBAMMFG1ETCBJc3N1ZXIgSUFDQSBSb290MB4X\r\nDTI2MDUxODA2MTkyNloXDTI3MDgxODA2MTkyNlowMjELMAkGA1UEBhMCQVQxIzAh\r\nBgNVBAMMGm1ETCBJc3N1ZXIgRG9jdW1lbnQgU2lnbmVyMFkwEwYHKoZIzj0CAQYI\r\nKoZIzj0DAQcDQgAEyfmbtCsdQQ/4EDPg0Gt98gJYk1zkNOpGM9bHYEi49mcwEICy\r\nJ3iSfvHoo/fVvDNGnTzQPpeVtHYbGaZzvsIo+aOByjCBxzAfBgNVHSMEGDAWgBRy\r\nsvdvSdC8s91wzC1kmaM3gALo4zAdBgNVHQ4EFgQUXGTWuUtFOLFFNFJbOULu+PhB\r\ngn0wDgYDVR0PAQH/BAQDAgeAMCcGA1UdEgQgMB6GHGh0dHBzOi8vaWFjYS50ZW5h\r\nbnQxLndhbHQuaWQwFQYDVR0lAQH/BAswCQYHKIGMXQUBAjA1BgNVHR8ELjAsMCqg\r\nKKAmhiRodHRwczovL2NybC50ZW5hbnQxLndhbHQuaWQvaWFjYS5jcmwwCgYIKoZI\r\nzj0EAwIDSAAwRQIgBsfq7TjUHVPTUiTVtCDKQRCdXzp6/eRzqOKH7YaIo9cCIQDr\r\neIKTeCNxH5vJCeP4iGFDbaKWuIIGbO1/32bwV/CvOQ==\n-----END CERTIFICATE-----",
"signer_jwk": {
"kty": "EC",
"crv": "P-256",
"x": "yfmbtCsdQQ_4EDPg0Gt98gJYk1zkNOpGM9bHYEi49mc",
"y": "MBCAsid4kn7x6KP31bwzRp080D6XlbR2Gxmmc77CKPk"
}
},
"errors": [],
"execution_time": "PT0.004215542S"
},
"mso_mdoc/issuer_signed_integrity": {
"policy_executed": {
"policy": "mso_mdoc/issuer_signed_integrity",
"id": "mso_mdoc/issuer_signed_integrity",
"description": "Verify issuer-verified data integrity"
},
"success": true,
"results": {
"namespace": {
"org.iso.18013.5.1": [
{
"id": "family_name",
"digest_id": 0,
"value": "Doe",
"value_type": "CborString",
"random_hex": "736b060d21fa4e37f17b0b0568285add9d064ba883eb6c35",
"serialized_hex": "a4686469676573744944006672616e646f6d5818736b060d21fa4e37f17b0b0568285add9d064ba883eb6c3571656c656d656e744964656e7469666965726b66616d696c795f6e616d656c656c656d656e7456616c756563446f65"
},
{
"id": "given_name",
"digest_id": 1,
"value": "John",
"value_type": "CborString",
"random_hex": "312e734c986fdf279c7d75130b0da4794d16d44bbb3cfd61",
"serialized_hex": "a4686469676573744944016672616e646f6d5818312e734c986fdf279c7d75130b0da4794d16d44bbb3cfd6171656c656d656e744964656e7469666965726a676976656e5f6e616d656c656c656d656e7456616c7565644a6f686e"
},
{
"id": "birth_date",
"digest_id": 2,
"value": "1986-03-22",
"value_type": "CborString",
"random_hex": "e6864d13d13a9fb1a7fccb729a6330dec975bac5e79fa023",
"serialized_hex": "a4686469676573744944026672616e646f6d5818e6864d13d13a9fb1a7fccb729a6330dec975bac5e79fa02371656c656d656e744964656e7469666965726a62697274685f646174656c656c656d656e7456616c7565d903ec6a313938362d30332d3232"
}
]
},
"matching_digest": {
"org.iso.18013.5.1": [
"family_name",
"given_name",
"birth_date"
]
}
},
"errors": [],
"execution_time": "PT0.003104834S"
},
"mso_mdoc/mso": {
"policy_executed": {
"policy": "mso_mdoc/mso",
"id": "mso_mdoc/mso",
"description": "Verify MSO"
},
"success": true,
"results": {
"signed": "2026-05-18T07:48:27.870905006Z",
"valid_from": "2027-05-18T07:48:27.852289522Z"
},
"errors": [],
"execution_time": "PT0.004245834S"
}
}
},
"vc_policies": [
{
"policy": {
"policy": "signature",
"id": "signature"
},
"success": true,
"result": {
"verification_result": true,
"signed_credential": "o2d2ZXJzaW9uYzEuMGlkb2N1bWVudHOBo2dkb2NUeXBldW9yZy5pc28uMTgwMTMuNS4xLm1ETGxpc3N1ZXJTaWduZWSiam5hbWVTcGFjZXOhcW9yZy5pc28uMTgwMTMuNS4xg9gYWFukaGRpZ2VzdElEAGZyYW5kb21YGHNrBg0h-k438XsLBWgoWt2dBkuog-tsNXFlbGVtZW50SWRlbnRpZmllcmtmYW1pbHlfbmFtZWxlbGVtZW50VmFsdWVjRG9l2BhYW6RoZGlnZXN0SUQBZnJhbmRvbVgYMS5zTJhv3yecfXUTCw2keU0W1Eu7PP1hcWVsZW1lbnRJZGVudGlmaWVyamdpdmVuX25hbWVsZWxlbWVudFZhbHVlZEpvaG7YGFhkpGhkaWdlc3RJRAJmcmFuZG9tWBjmhk0T0Tqfsaf8y3KaYzDeyXW6xeefoCNxZWxlbWVudElkZW50aWZpZXJqYmlydGhfZGF0ZWxlbGVtZW50VmFsdWXZA-xqMTk4Ni0wMy0yMmppc3N1ZXJBdXRohEOhASahGCFZAi8wggIrMIIB0aADAgECAhRED-gfURxpHvIfRtUe91K73mZpljAKBggqhkjOPQQDAjAsMQswCQYDVQQGEwJBVDEdMBsGA1UEAwwUbURMIElzc3VlciBJQUNBIFJvb3QwHhcNMjYwNTE4MDYxOTI2WhcNMjcwODE4MDYxOTI2WjAyMQswCQYDVQQGEwJBVDEjMCEGA1UEAwwabURMIElzc3VlciBEb2N1bWVudCBTaWduZXIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATJ-Zu0Kx1BD_gQM-DQa33yAliTXOQ06kYz1sdgSLj2ZzAQgLIneJJ-8eij99W8M0adPNA-l5W0dhsZpnO-wij5o4HKMIHHMB8GA1UdIwQYMBaAFHKy929J0Lyz3XDMLWSZozeAAujjMB0GA1UdDgQWBBRcZNa5S0U4sUU0Uls5Qu74-EGCfTAOBgNVHQ8BAf8EBAMCB4AwJwYDVR0SBCAwHoYcaHR0cHM6Ly9pYWNhLnRlbmFudDEud2FsdC5pZDAVBgNVHSUBAf8ECzAJBgcogYxdBQECMDUGA1UdHwQuMCwwKqAooCaGJGh0dHBzOi8vY3JsLnRlbmFudDEud2FsdC5pZC9pYWNhLmNybDAKBggqhkjOPQQDAgNIADBFAiAGx-rtONQdU9NSJNW0IMpBEJ1fOnr95HOo4ofthoij1wIhAOt4gpN4I3Efm8kJ4_iIYUNtopa4ggZs7X_fZvBX8K85WQKV2BhZApCmZ3ZlcnNpb25jMS4wb2RpZ2VzdEFsZ29yaXRobWdTSEEtMjU2bHZhbHVlRGlnZXN0c6Fxb3JnLmlzby4xODAxMy41LjGqAFgglIt2JC3rDbrr6azX5GWPMGyGqyD5gTnyASk37pDhZQYBWCCMQ7Ph45igVnLbvCtlSqLPlaMTXSk2kLoy4Mxn1x37kwJYIOjGjCHBlfvZi74lANZXAWt_FDdDf6NU591eYMGbFladA1ggcdl99l7hzcOFDYUeghGX0gDXYYgEN7WyRFrgSSFVvv4EWCCKwt6H37DEK8kJFcs__AiFZzLDc-4GPFKuYYEJzv_X1QVYIMybLDIPi3G7go7Bq4ggn0UYXEpmKMazxXZH25mnl0RQBlggofgVz7C7i4z0Iz8zrEPhNBRMlZcr4EhqQCR2pjo3qsoHWCDaJ4DszfMYhI0HfyOcSgMEgx_K12dx34khKW6ohKRhZghYIPTE7TeLsXVDSX9l3DTPB4zDhzF1WDTmCw3Ecxv1deNJCVggqueG-PUmdP8sJdsQ6XbuByQzAZXoRdwB-7dn0hxtkCdtZGV2aWNlS2V5SW5mb6FpZGV2aWNlS2V5owEBIAYhWCCPFDBFXu0dzdb8mQpWwNkvv8O0_jPI0m1DhivXPv4x3mdkb2NUeXBldW9yZy5pc28uMTgwMTMuNS4xLm1ETGx2YWxpZGl0eUluZm-jZnNpZ25lZMB4HjIwMjYtMDUtMThUMDc6NDg6MjcuODcwOTA1MDA2Wml2YWxpZEZyb23AeB4yMDI2LTA1LTE4VDA3OjQ4OjI3Ljg3MDkwNTAwNlpqdmFsaWRVbnRpbMB4HjIwMjctMDUtMThUMDc6NDg6MjcuODUyMjg5NTIyWlhAmRC5DQjAwaNaozniXwKmOoMscEyNwA65Y31zyoCJpiGiZxs-fk9bcsZnrDSu5ATPVEIKrf_4hjY8ODArK76q2WxkZXZpY2VTaWduZWSiam5hbWVTcGFjZXPYGEGgamRldmljZUF1dGihb2RldmljZVNpZ25hdHVyZYRDoQEnoPZYQGQUayxOBk2Wp7g7UhumPbtFSQ5h1h4jArb60UGZOsFMvG6MXHBBPd4JdOX40auhbmKNmBjinPfw96QN9XfV-Q9mc3RhdHVzAA",
"credential_signature": {
"type": "signature-cose",
"signerKey": {
"type": "jwk",
"jwk": {
"kty": "EC",
"crv": "P-256",
"x": "yfmbtCsdQQ_4EDPg0Gt98gJYk1zkNOpGM9bHYEi49mc",
"y": "MBCAsid4kn7x6KP31bwzRp080D6XlbR2Gxmmc77CKPk"
}
},
"x5cList": [
"MIICKzCCAdGgAwIBAgIURA/oH1EcaR7yH0bVHvdSu95maZYwCgYIKoZIzj0EAwIwLDELMAkGA1UEBhMCQVQxHTAbBgNVBAMMFG1ETCBJc3N1ZXIgSUFDQSBSb290MB4XDTI2MDUxODA2MTkyNloXDTI3MDgxODA2MTkyNlowMjELMAkGA1UEBhMCQVQxIzAhBgNVBAMMGm1ETCBJc3N1ZXIgRG9jdW1lbnQgU2lnbmVyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEyfmbtCsdQQ/4EDPg0Gt98gJYk1zkNOpGM9bHYEi49mcwEICyJ3iSfvHoo/fVvDNGnTzQPpeVtHYbGaZzvsIo+aOByjCBxzAfBgNVHSMEGDAWgBRysvdvSdC8s91wzC1kmaM3gALo4zAdBgNVHQ4EFgQUXGTWuUtFOLFFNFJbOULu+PhBgn0wDgYDVR0PAQH/BAQDAgeAMCcGA1UdEgQgMB6GHGh0dHBzOi8vaWFjYS50ZW5hbnQxLndhbHQuaWQwFQYDVR0lAQH/BAswCQYHKIGMXQUBAjA1BgNVHR8ELjAsMCqgKKAmhiRodHRwczovL2NybC50ZW5hbnQxLndhbHQuaWQvaWFjYS5jcmwwCgYIKoZIzj0EAwIDSAAwRQIgBsfq7TjUHVPTUiTVtCDKQRCdXzp6/eRzqOKH7YaIo9cCIQDreIKTeCNxH5vJCeP4iGFDbaKWuIIGbO1/32bwV/CvOQ=="
]
},
"verified_data": {
"docType": "org.iso.18013.5.1.mDL",
"org.iso.18013.5.1": {
"family_name": "Doe",
"given_name": "John",
"birth_date": "1986-03-22"
}
},
"successful_issuer_public_key": {
"kty": "EC",
"crv": "P-256",
"x": "yfmbtCsdQQ_4EDPg0Gt98gJYk1zkNOpGM9bHYEi49mc",
"y": "MBCAsid4kn7x6KP31bwzRp080D6XlbR2Gxmmc77CKPk"
},
"successful_issuer_public_key_id": "FOWc2X5ZSle6u66RwXQoVUwm86vnTnbZ61s_Golepz0"
}
}
],
"specific_vc_policies": {},
"overallSuccess": true
},
"presentation_validation_results": {
"credential_1": {
"mso_mdoc/device-auth": {
"policy_executed": {
"policy": "mso_mdoc/device-auth",
"id": "mso_mdoc/device-auth",
"description": "Verify device authentication"
},
"success": true,
"results": {
"device_public_jwk": {
"kty": "OKP",
"crv": "Ed25519",
"x": "jxQwRV7tHc3W_JkKVsDZL7_DtP4zyNJtQ4Yr1z7-Md4"
},
"device_auth_bytes_hex": "d8185868847444657669636541757468656e7469636174696f6e83f6f682714f70656e494434565048616e646f7665725820bdb670da65dae163a8fe31c591c82da53c6da1b38232c8f4d3657405e4220a43756f72672e69736f2e31383031332e352e312e6d444cd81841a0"
},
"errors": [],
"execution_time": "PT0.002658542S"
},
"mso_mdoc/device_key_auth": {
"policy_executed": {
"policy": "mso_mdoc/device_key_auth",
"id": "mso_mdoc/device_key_auth",
"description": "Verify holder-verified data"
},
"success": true,
"results": {
"empty_device_signed_namespaces": true
},
"errors": [],
"execution_time": "PT0.000800334S"
},
"mso_mdoc/issuer_auth": {
"policy_executed": {
"policy": "mso_mdoc/issuer_auth",
"id": "mso_mdoc/issuer_auth",
"description": "Verify issuer authentication"
},
"success": true,
"results": {
"certificate_chain": [
"3082022b308201d1a0030201020214440fe81f511c691ef21f46d51ef752bbde666996300a06082a8648ce3d040302302c310b3009060355040613024154311d301b06035504030c146d444c20497373756572204941434120526f6f74301e170d3236303531383036313932365a170d3237303831383036313932365a3032310b30090603550406130241543123302106035504030c1a6d444c2049737375657220446f63756d656e74205369676e65723059301306072a8648ce3d020106082a8648ce3d03010703420004c9f99bb42b1d410ff81033e0d06b7df20258935ce434ea4633d6c76048b8f667301080b22778927ef1e8a3f7d5bc33469d3cd03e9795b4761b19a673bec228f9a381ca3081c7301f0603551d2304183016801472b2f76f49d0bcb3dd70cc2d6499a3378002e8e3301d0603551d0e041604145c64d6b94b4538b14534525b3942eef8f841827d300e0603551d0f0101ff04040302078030270603551d120420301e861c68747470733a2f2f696163612e74656e616e74312e77616c742e696430150603551d250101ff040b3009060728818c5d05010230350603551d1f042e302c302aa028a026862468747470733a2f2f63726c2e74656e616e74312e77616c742e69642f696163612e63726c300a06082a8648ce3d0403020348003045022006c7eaed38d41d53d35224d5b420ca41109d5f3a7afde473a8e287ed8688a3d7022100eb7882937823711f9bc909e3f88861436da296b882066ced7fdf66f057f0af39"
],
"signer_pem": "-----BEGIN CERTIFICATE-----\nMIICKzCCAdGgAwIBAgIURA/oH1EcaR7yH0bVHvdSu95maZYwCgYIKoZIzj0EAwIw\r\nLDELMAkGA1UEBhMCQVQxHTAbBgNVBAMMFG1ETCBJc3N1ZXIgSUFDQSBSb290MB4X\r\nDTI2MDUxODA2MTkyNloXDTI3MDgxODA2MTkyNlowMjELMAkGA1UEBhMCQVQxIzAh\r\nBgNVBAMMGm1ETCBJc3N1ZXIgRG9jdW1lbnQgU2lnbmVyMFkwEwYHKoZIzj0CAQYI\r\nKoZIzj0DAQcDQgAEyfmbtCsdQQ/4EDPg0Gt98gJYk1zkNOpGM9bHYEi49mcwEICy\r\nJ3iSfvHoo/fVvDNGnTzQPpeVtHYbGaZzvsIo+aOByjCBxzAfBgNVHSMEGDAWgBRy\r\nsvdvSdC8s91wzC1kmaM3gALo4zAdBgNVHQ4EFgQUXGTWuUtFOLFFNFJbOULu+PhB\r\ngn0wDgYDVR0PAQH/BAQDAgeAMCcGA1UdEgQgMB6GHGh0dHBzOi8vaWFjYS50ZW5h\r\nbnQxLndhbHQuaWQwFQYDVR0lAQH/BAswCQYHKIGMXQUBAjA1BgNVHR8ELjAsMCqg\r\nKKAmhiRodHRwczovL2NybC50ZW5hbnQxLndhbHQuaWQvaWFjYS5jcmwwCgYIKoZI\r\nzj0EAwIDSAAwRQIgBsfq7TjUHVPTUiTVtCDKQRCdXzp6/eRzqOKH7YaIo9cCIQDr\r\neIKTeCNxH5vJCeP4iGFDbaKWuIIGbO1/32bwV/CvOQ==\n-----END CERTIFICATE-----",
"signer_jwk": {
"kty": "EC",
"crv": "P-256",
"x": "yfmbtCsdQQ_4EDPg0Gt98gJYk1zkNOpGM9bHYEi49mc",
"y": "MBCAsid4kn7x6KP31bwzRp080D6XlbR2Gxmmc77CKPk"
}
},
"errors": [],
"execution_time": "PT0.004215542S"
},
"mso_mdoc/issuer_signed_integrity": {
"policy_executed": {
"policy": "mso_mdoc/issuer_signed_integrity",
"id": "mso_mdoc/issuer_signed_integrity",
"description": "Verify issuer-verified data integrity"
},
"success": true,
"results": {
"namespace": {
"org.iso.18013.5.1": [
{
"id": "family_name",
"digest_id": 0,
"value": "Doe",
"value_type": "CborString",
"random_hex": "736b060d21fa4e37f17b0b0568285add9d064ba883eb6c35",
"serialized_hex": "a4686469676573744944006672616e646f6d5818736b060d21fa4e37f17b0b0568285add9d064ba883eb6c3571656c656d656e744964656e7469666965726b66616d696c795f6e616d656c656c656d656e7456616c756563446f65"
},
{
"id": "given_name",
"digest_id": 1,
"value": "John",
"value_type": "CborString",
"random_hex": "312e734c986fdf279c7d75130b0da4794d16d44bbb3cfd61",
"serialized_hex": "a4686469676573744944016672616e646f6d5818312e734c986fdf279c7d75130b0da4794d16d44bbb3cfd6171656c656d656e744964656e7469666965726a676976656e5f6e616d656c656c656d656e7456616c7565644a6f686e"
},
{
"id": "birth_date",
"digest_id": 2,
"value": "1986-03-22",
"value_type": "CborString",
"random_hex": "e6864d13d13a9fb1a7fccb729a6330dec975bac5e79fa023",
"serialized_hex": "a4686469676573744944026672616e646f6d5818e6864d13d13a9fb1a7fccb729a6330dec975bac5e79fa02371656c656d656e744964656e7469666965726a62697274685f646174656c656c656d656e7456616c7565d903ec6a313938362d30332d3232"
}
]
},
"matching_digest": {
"org.iso.18013.5.1": [
"family_name",
"given_name",
"birth_date"
]
}
},
"errors": [],
"execution_time": "PT0.003104834S"
},
"mso_mdoc/mso": {
"policy_executed": {
"policy": "mso_mdoc/mso",
"id": "mso_mdoc/mso",
"description": "Verify MSO"
},
"success": true,
"results": {
"signed": "2026-05-18T07:48:27.870905006Z",
"valid_from": "2027-05-18T07:48:27.852289522Z"
},
"errors": [],
"execution_time": "PT0.004245834S"
}
}
},
"presented_raw_data": {
"vpToken": {
"credential_1": [
"o2d2ZXJzaW9uYzEuMGlkb2N1bWVudHOBo2dkb2NUeXBldW9yZy5pc28uMTgwMTMuNS4xLm1ETGxpc3N1ZXJTaWduZWSiam5hbWVTcGFjZXOhcW9yZy5pc28uMTgwMTMuNS4xg9gYWFukaGRpZ2VzdElEAGZyYW5kb21YGHNrBg0h-k438XsLBWgoWt2dBkuog-tsNXFlbGVtZW50SWRlbnRpZmllcmtmYW1pbHlfbmFtZWxlbGVtZW50VmFsdWVjRG9l2BhYW6RoZGlnZXN0SUQBZnJhbmRvbVgYMS5zTJhv3yecfXUTCw2keU0W1Eu7PP1hcWVsZW1lbnRJZGVudGlmaWVyamdpdmVuX25hbWVsZWxlbWVudFZhbHVlZEpvaG7YGFhkpGhkaWdlc3RJRAJmcmFuZG9tWBjmhk0T0Tqfsaf8y3KaYzDeyXW6xeefoCNxZWxlbWVudElkZW50aWZpZXJqYmlydGhfZGF0ZWxlbGVtZW50VmFsdWXZA-xqMTk4Ni0wMy0yMmppc3N1ZXJBdXRohEOhASahGCFZAi8wggIrMIIB0aADAgECAhRED-gfURxpHvIfRtUe91K73mZpljAKBggqhkjOPQQDAjAsMQswCQYDVQQGEwJBVDEdMBsGA1UEAwwUbURMIElzc3VlciBJQUNBIFJvb3QwHhcNMjYwNTE4MDYxOTI2WhcNMjcwODE4MDYxOTI2WjAyMQswCQYDVQQGEwJBVDEjMCEGA1UEAwwabURMIElzc3VlciBEb2N1bWVudCBTaWduZXIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATJ-Zu0Kx1BD_gQM-DQa33yAliTXOQ06kYz1sdgSLj2ZzAQgLIneJJ-8eij99W8M0adPNA-l5W0dhsZpnO-wij5o4HKMIHHMB8GA1UdIwQYMBaAFHKy929J0Lyz3XDMLWSZozeAAujjMB0GA1UdDgQWBBRcZNa5S0U4sUU0Uls5Qu74-EGCfTAOBgNVHQ8BAf8EBAMCB4AwJwYDVR0SBCAwHoYcaHR0cHM6Ly9pYWNhLnRlbmFudDEud2FsdC5pZDAVBgNVHSUBAf8ECzAJBgcogYxdBQECMDUGA1UdHwQuMCwwKqAooCaGJGh0dHBzOi8vY3JsLnRlbmFudDEud2FsdC5pZC9pYWNhLmNybDAKBggqhkjOPQQDAgNIADBFAiAGx-rtONQdU9NSJNW0IMpBEJ1fOnr95HOo4ofthoij1wIhAOt4gpN4I3Efm8kJ4_iIYUNtopa4ggZs7X_fZvBX8K85WQKV2BhZApCmZ3ZlcnNpb25jMS4wb2RpZ2VzdEFsZ29yaXRobWdTSEEtMjU2bHZhbHVlRGlnZXN0c6Fxb3JnLmlzby4xODAxMy41LjGqAFgglIt2JC3rDbrr6azX5GWPMGyGqyD5gTnyASk37pDhZQYBWCCMQ7Ph45igVnLbvCtlSqLPlaMTXSk2kLoy4Mxn1x37kwJYIOjGjCHBlfvZi74lANZXAWt_FDdDf6NU591eYMGbFladA1ggcdl99l7hzcOFDYUeghGX0gDXYYgEN7WyRFrgSSFVvv4EWCCKwt6H37DEK8kJFcs__AiFZzLDc-4GPFKuYYEJzv_X1QVYIMybLDIPi3G7go7Bq4ggn0UYXEpmKMazxXZH25mnl0RQBlggofgVz7C7i4z0Iz8zrEPhNBRMlZcr4EhqQCR2pjo3qsoHWCDaJ4DszfMYhI0HfyOcSgMEgx_K12dx34khKW6ohKRhZghYIPTE7TeLsXVDSX9l3DTPB4zDhzF1WDTmCw3Ecxv1deNJCVggqueG-PUmdP8sJdsQ6XbuByQzAZXoRdwB-7dn0hxtkCdtZGV2aWNlS2V5SW5mb6FpZGV2aWNlS2V5owEBIAYhWCCPFDBFXu0dzdb8mQpWwNkvv8O0_jPI0m1DhivXPv4x3mdkb2NUeXBldW9yZy5pc28uMTgwMTMuNS4xLm1ETGx2YWxpZGl0eUluZm-jZnNpZ25lZMB4HjIwMjYtMDUtMThUMDc6NDg6MjcuODcwOTA1MDA2Wml2YWxpZEZyb23AeB4yMDI2LTA1LTE4VDA3OjQ4OjI3Ljg3MDkwNTAwNlpqdmFsaWRVbnRpbMB4HjIwMjctMDUtMThUMDc6NDg6MjcuODUyMjg5NTIyWlhAmRC5DQjAwaNaozniXwKmOoMscEyNwA65Y31zyoCJpiGiZxs-fk9bcsZnrDSu5ATPVEIKrf_4hjY8ODArK76q2WxkZXZpY2VTaWduZWSiam5hbWVTcGFjZXPYGEGgamRldmljZUF1dGihb2RldmljZVNpZ25hdHVyZYRDoQEnoPZYQGQUayxOBk2Wp7g7UhumPbtFSQ5h1h4jArb60UGZOsFMvG6MXHBBPd4JdOX40auhbmKNmBjinPfw96QN9XfV-Q9mc3RhdHVzAA"
]
},
"state": "da82aed7-ea36-4303-9c4a-3ded03cdedb4"
},
"presented_presentations": {
"credential_1": {
"type": "mso_mdoc",
"format": "mso_mdoc",
"mdoc": {
"credentialData": {
"docType": "org.iso.18013.5.1.mDL",
"org.iso.18013.5.1": {
"family_name": "Doe",
"given_name": "John",
"birth_date": "1986-03-22"
}
},
"signed": "o2d2ZXJzaW9uYzEuMGlkb2N1bWVudHOBo2dkb2NUeXBldW9yZy5pc28uMTgwMTMuNS4xLm1ETGxpc3N1ZXJTaWduZWSiam5hbWVTcGFjZXOhcW9yZy5pc28uMTgwMTMuNS4xg9gYWFukaGRpZ2VzdElEAGZyYW5kb21YGHNrBg0h-k438XsLBWgoWt2dBkuog-tsNXFlbGVtZW50SWRlbnRpZmllcmtmYW1pbHlfbmFtZWxlbGVtZW50VmFsdWVjRG9l2BhYW6RoZGlnZXN0SUQBZnJhbmRvbVgYMS5zTJhv3yecfXUTCw2keU0W1Eu7PP1hcWVsZW1lbnRJZGVudGlmaWVyamdpdmVuX25hbWVsZWxlbWVudFZhbHVlZEpvaG7YGFhkpGhkaWdlc3RJRAJmcmFuZG9tWBjmhk0T0Tqfsaf8y3KaYzDeyXW6xeefoCNxZWxlbWVudElkZW50aWZpZXJqYmlydGhfZGF0ZWxlbGVtZW50VmFsdWXZA-xqMTk4Ni0wMy0yMmppc3N1ZXJBdXRohEOhASahGCFZAi8wggIrMIIB0aADAgECAhRED-gfURxpHvIfRtUe91K73mZpljAKBggqhkjOPQQDAjAsMQswCQYDVQQGEwJBVDEdMBsGA1UEAwwUbURMIElzc3VlciBJQUNBIFJvb3QwHhcNMjYwNTE4MDYxOTI2WhcNMjcwODE4MDYxOTI2WjAyMQswCQYDVQQGEwJBVDEjMCEGA1UEAwwabURMIElzc3VlciBEb2N1bWVudCBTaWduZXIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATJ-Zu0Kx1BD_gQM-DQa33yAliTXOQ06kYz1sdgSLj2ZzAQgLIneJJ-8eij99W8M0adPNA-l5W0dhsZpnO-wij5o4HKMIHHMB8GA1UdIwQYMBaAFHKy929J0Lyz3XDMLWSZozeAAujjMB0GA1UdDgQWBBRcZNa5S0U4sUU0Uls5Qu74-EGCfTAOBgNVHQ8BAf8EBAMCB4AwJwYDVR0SBCAwHoYcaHR0cHM6Ly9pYWNhLnRlbmFudDEud2FsdC5pZDAVBgNVHSUBAf8ECzAJBgcogYxdBQECMDUGA1UdHwQuMCwwKqAooCaGJGh0dHBzOi8vY3JsLnRlbmFudDEud2FsdC5pZC9pYWNhLmNybDAKBggqhkjOPQQDAgNIADBFAiAGx-rtONQdU9NSJNW0IMpBEJ1fOnr95HOo4ofthoij1wIhAOt4gpN4I3Efm8kJ4_iIYUNtopa4ggZs7X_fZvBX8K85WQKV2BhZApCmZ3ZlcnNpb25jMS4wb2RpZ2VzdEFsZ29yaXRobWdTSEEtMjU2bHZhbHVlRGlnZXN0c6Fxb3JnLmlzby4xODAxMy41LjGqAFgglIt2JC3rDbrr6azX5GWPMGyGqyD5gTnyASk37pDhZQYBWCCMQ7Ph45igVnLbvCtlSqLPlaMTXSk2kLoy4Mxn1x37kwJYIOjGjCHBlfvZi74lANZXAWt_FDdDf6NU591eYMGbFladA1ggcdl99l7hzcOFDYUeghGX0gDXYYgEN7WyRFrgSSFVvv4EWCCKwt6H37DEK8kJFcs__AiFZzLDc-4GPFKuYYEJzv_X1QVYIMybLDIPi3G7go7Bq4ggn0UYXEpmKMazxXZH25mnl0RQBlggofgVz7C7i4z0Iz8zrEPhNBRMlZcr4EhqQCR2pjo3qsoHWCDaJ4DszfMYhI0HfyOcSgMEgx_K12dx34khKW6ohKRhZghYIPTE7TeLsXVDSX9l3DTPB4zDhzF1WDTmCw3Ecxv1deNJCVggqueG-PUmdP8sJdsQ6XbuByQzAZXoRdwB-7dn0hxtkCdtZGV2aWNlS2V5SW5mb6FpZGV2aWNlS2V5owEBIAYhWCCPFDBFXu0dzdb8mQpWwNkvv8O0_jPI0m1DhivXPv4x3mdkb2NUeXBldW9yZy5pc28uMTgwMTMuNS4xLm1ETGx2YWxpZGl0eUluZm-jZnNpZ25lZMB4HjIwMjYtMDUtMThUMDc6NDg6MjcuODcwOTA1MDA2Wml2YWxpZEZyb23AeB4yMDI2LTA1LTE4VDA3OjQ4OjI3Ljg3MDkwNTAwNlpqdmFsaWRVbnRpbMB4HjIwMjctMDUtMThUMDc6NDg6MjcuODUyMjg5NTIyWlhAmRC5DQjAwaNaozniXwKmOoMscEyNwA65Y31zyoCJpiGiZxs-fk9bcsZnrDSu5ATPVEIKrf_4hjY8ODArK76q2WxkZXZpY2VTaWduZWSiam5hbWVTcGFjZXPYGEGgamRldmljZUF1dGihb2RldmljZVNpZ25hdHVyZYRDoQEnoPZYQGQUayxOBk2Wp7g7UhumPbtFSQ5h1h4jArb60UGZOsFMvG6MXHBBPd4JdOX40auhbmKNmBjinPfw96QN9XfV-Q9mc3RhdHVzAA",
"docType": "org.iso.18013.5.1.mDL",
"signature": {
"type": "signature-cose",
"signerKey": {
"type": "jwk",
"jwk": {
"kty": "EC",
"crv": "P-256",
"x": "yfmbtCsdQQ_4EDPg0Gt98gJYk1zkNOpGM9bHYEi49mc",
"y": "MBCAsid4kn7x6KP31bwzRp080D6XlbR2Gxmmc77CKPk"
}
},
"x5cList": [
"MIICKzCCAdGgAwIBAgIURA/oH1EcaR7yH0bVHvdSu95maZYwCgYIKoZIzj0EAwIwLDELMAkGA1UEBhMCQVQxHTAbBgNVBAMMFG1ETCBJc3N1ZXIgSUFDQSBSb290MB4XDTI2MDUxODA2MTkyNloXDTI3MDgxODA2MTkyNlowMjELMAkGA1UEBhMCQVQxIzAhBgNVBAMMGm1ETCBJc3N1ZXIgRG9jdW1lbnQgU2lnbmVyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEyfmbtCsdQQ/4EDPg0Gt98gJYk1zkNOpGM9bHYEi49mcwEICyJ3iSfvHoo/fVvDNGnTzQPpeVtHYbGaZzvsIo+aOByjCBxzAfBgNVHSMEGDAWgBRysvdvSdC8s91wzC1kmaM3gALo4zAdBgNVHQ4EFgQUXGTWuUtFOLFFNFJbOULu+PhBgn0wDgYDVR0PAQH/BAQDAgeAMCcGA1UdEgQgMB6GHGh0dHBzOi8vaWFjYS50ZW5hbnQxLndhbHQuaWQwFQYDVR0lAQH/BAswCQYHKIGMXQUBAjA1BgNVHR8ELjAsMCqgKKAmhiRodHRwczovL2NybC50ZW5hbnQxLndhbHQuaWQvaWFjYS5jcmwwCgYIKoZIzj0EAwIDSAAwRQIgBsfq7TjUHVPTUiTVtCDKQRCdXzp6/eRzqOKH7YaIo9cCIQDreIKTeCNxH5vJCeP4iGFDbaKWuIIGbO1/32bwV/CvOQ=="
]
},
"format": "mso_mdoc",
"mso": {
"version": "1.0",
"digestAlgorithm": "SHA-256",
"valueDigests": {
"org.iso.18013.5.1": {
"0": [
-108,
-117,
118,
36,
45,
-21,
13,
-70,
-21,
-23,
-84,
-41,
-28,
101,
-113,
48,
108,
-122,
-85,
32,
-7,
-127,
57,
-14,
1,
41,
55,
-18,
-112,
-31,
101,
6
],
"1": [
-116,
67,
-77,
-31,
-29,
-104,
-96,
86,
114,
-37,
-68,
43,
101,
74,
-94,
-49,
-107,
-93,
19,
93,
41,
54,
-112,
-70,
50,
-32,
-52,
103,
-41,
29,
-5,
-109
],
"2": [
-24,
-58,
-116,
33,
-63,
-107,
-5,
-39,
-117,
-66,
37,
0,
-42,
87,
1,
107,
127,
20,
55,
67,
127,
-93,
84,
-25,
-35,
94,
96,
-63,
-101,
22,
86,
-99
],
"3": [
113,
-39,
125,
-10,
94,
-31,
-51,
-61,
-123,
13,
-123,
30,
-126,
17,
-105,
-46,
0,
-41,
97,
-120,
4,
55,
-75,
-78,
68,
90,
-32,
73,
33,
85,
-66,
-2
],
"4": [
-118,
-62,
-34,
-121,
-33,
-80,
-60,
43,
-55,
9,
21,
-53,
63,
-4,
8,
-123,
103,
50,
-61,
115,
-18,
6,
60,
82,
-82,
97,
-127,
9,
-50,
-1,
-41,
-43
],
"5": [
-52,
-101,
44,
50,
15,
-117,
113,
-69,
-126,
-114,
-63,
-85,
-120,
32,
-97,
69,
24,
92,
74,
102,
40,
-58,
-77,
-59,
118,
71,
-37,
-103,
-89,
-105,
68,
80
],
"6": [
-95,
-8,
21,
-49,
-80,
-69,
-117,
-116,
-12,
35,
63,
51,
-84,
67,
-31,
52,
20,
76,
-107,
-105,
43,
-32,
72,
106,
64,
36,
118,
-90,
58,
55,
-86,
-54
],
"7": [
-38,
39,
-128,
-20,
-51,
-13,
24,
-124,
-115,
7,
127,
35,
-100,
74,
3,
4,
-125,
31,
-54,
-41,
103,
113,
-33,
-119,
33,
41,
110,
-88,
-124,
-92,
97,
102
],
"8": [
-12,
-60,
-19,
55,
-117,
-79,
117,
67,
73,
127,
101,
-36,
52,
-49,
7,
-116,
-61,
-121,
49,
117,
88,
52,
-26,
11,
13,
-60,
115,
27,
-11,
117,
-29,
73
],
"9": [
-86,
-25,
-122,
-8,
-11,
38,
116,
-1,
44,
37,
-37,
16,
-23,
118,
-18,
7,
36,
51,
1,
-107,
-24,
69,
-36,
1,
-5,
-73,
103,
-46,
28,
109,
-112,
39
]
}
},
"deviceKeyInfo": {
"deviceKey": {
"kty": 1,
"crv": 6,
"x": [
-113,
20,
48,
69,
94,
-19,
29,
-51,
-42,
-4,
-103,
10,
86,
-64,
-39,
47,
-65,
-61,
-76,
-2,
51,
-56,
-46,
109,
67,
-122,
43,
-41,
62,
-2,
49,
-34
]
}
},
"docType": "org.iso.18013.5.1.mDL",
"validityInfo": {
"signed": "2026-05-18T07:48:27.870905006Z",
"validFrom": "2026-05-18T07:48:27.870905006Z",
"validUntil": "2027-05-18T07:48:27.852289522Z"
}
}
}
}
},
"presented_credentials": {
"credential_1": [
{
"type": "vc-mdocs",
"credentialData": {
"docType": "org.iso.18013.5.1.mDL",
"org.iso.18013.5.1": {
"family_name": "Doe",
"given_name": "John",
"birth_date": "1986-03-22"
}
},
"signed": "o2d2ZXJzaW9uYzEuMGlkb2N1bWVudHOBo2dkb2NUeXBldW9yZy5pc28uMTgwMTMuNS4xLm1ETGxpc3N1ZXJTaWduZWSiam5hbWVTcGFjZXOhcW9yZy5pc28uMTgwMTMuNS4xg9gYWFukaGRpZ2VzdElEAGZyYW5kb21YGHNrBg0h-k438XsLBWgoWt2dBkuog-tsNXFlbGVtZW50SWRlbnRpZmllcmtmYW1pbHlfbmFtZWxlbGVtZW50VmFsdWVjRG9l2BhYW6RoZGlnZXN0SUQBZnJhbmRvbVgYMS5zTJhv3yecfXUTCw2keU0W1Eu7PP1hcWVsZW1lbnRJZGVudGlmaWVyamdpdmVuX25hbWVsZWxlbWVudFZhbHVlZEpvaG7YGFhkpGhkaWdlc3RJRAJmcmFuZG9tWBjmhk0T0Tqfsaf8y3KaYzDeyXW6xeefoCNxZWxlbWVudElkZW50aWZpZXJqYmlydGhfZGF0ZWxlbGVtZW50VmFsdWXZA-xqMTk4Ni0wMy0yMmppc3N1ZXJBdXRohEOhASahGCFZAi8wggIrMIIB0aADAgECAhRED-gfURxpHvIfRtUe91K73mZpljAKBggqhkjOPQQDAjAsMQswCQYDVQQGEwJBVDEdMBsGA1UEAwwUbURMIElzc3VlciBJQUNBIFJvb3QwHhcNMjYwNTE4MDYxOTI2WhcNMjcwODE4MDYxOTI2WjAyMQswCQYDVQQGEwJBVDEjMCEGA1UEAwwabURMIElzc3VlciBEb2N1bWVudCBTaWduZXIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATJ-Zu0Kx1BD_gQM-DQa33yAliTXOQ06kYz1sdgSLj2ZzAQgLIneJJ-8eij99W8M0adPNA-l5W0dhsZpnO-wij5o4HKMIHHMB8GA1UdIwQYMBaAFHKy929J0Lyz3XDMLWSZozeAAujjMB0GA1UdDgQWBBRcZNa5S0U4sUU0Uls5Qu74-EGCfTAOBgNVHQ8BAf8EBAMCB4AwJwYDVR0SBCAwHoYcaHR0cHM6Ly9pYWNhLnRlbmFudDEud2FsdC5pZDAVBgNVHSUBAf8ECzAJBgcogYxdBQECMDUGA1UdHwQuMCwwKqAooCaGJGh0dHBzOi8vY3JsLnRlbmFudDEud2FsdC5pZC9pYWNhLmNybDAKBggqhkjOPQQDAgNIADBFAiAGx-rtONQdU9NSJNW0IMpBEJ1fOnr95HOo4ofthoij1wIhAOt4gpN4I3Efm8kJ4_iIYUNtopa4ggZs7X_fZvBX8K85WQKV2BhZApCmZ3ZlcnNpb25jMS4wb2RpZ2VzdEFsZ29yaXRobWdTSEEtMjU2bHZhbHVlRGlnZXN0c6Fxb3JnLmlzby4xODAxMy41LjGqAFgglIt2JC3rDbrr6azX5GWPMGyGqyD5gTnyASk37pDhZQYBWCCMQ7Ph45igVnLbvCtlSqLPlaMTXSk2kLoy4Mxn1x37kwJYIOjGjCHBlfvZi74lANZXAWt_FDdDf6NU591eYMGbFladA1ggcdl99l7hzcOFDYUeghGX0gDXYYgEN7WyRFrgSSFVvv4EWCCKwt6H37DEK8kJFcs__AiFZzLDc-4GPFKuYYEJzv_X1QVYIMybLDIPi3G7go7Bq4ggn0UYXEpmKMazxXZH25mnl0RQBlggofgVz7C7i4z0Iz8zrEPhNBRMlZcr4EhqQCR2pjo3qsoHWCDaJ4DszfMYhI0HfyOcSgMEgx_K12dx34khKW6ohKRhZghYIPTE7TeLsXVDSX9l3DTPB4zDhzF1WDTmCw3Ecxv1deNJCVggqueG-PUmdP8sJdsQ6XbuByQzAZXoRdwB-7dn0hxtkCdtZGV2aWNlS2V5SW5mb6FpZGV2aWNlS2V5owEBIAYhWCCPFDBFXu0dzdb8mQpWwNkvv8O0_jPI0m1DhivXPv4x3mdkb2NUeXBldW9yZy5pc28uMTgwMTMuNS4xLm1ETGx2YWxpZGl0eUluZm-jZnNpZ25lZMB4HjIwMjYtMDUtMThUMDc6NDg6MjcuODcwOTA1MDA2Wml2YWxpZEZyb23AeB4yMDI2LTA1LTE4VDA3OjQ4OjI3Ljg3MDkwNTAwNlpqdmFsaWRVbnRpbMB4HjIwMjctMDUtMThUMDc6NDg6MjcuODUyMjg5NTIyWlhAmRC5DQjAwaNaozniXwKmOoMscEyNwA65Y31zyoCJpiGiZxs-fk9bcsZnrDSu5ATPVEIKrf_4hjY8ODArK76q2WxkZXZpY2VTaWduZWSiam5hbWVTcGFjZXPYGEGgamRldmljZUF1dGihb2RldmljZVNpZ25hdHVyZYRDoQEnoPZYQGQUayxOBk2Wp7g7UhumPbtFSQ5h1h4jArb60UGZOsFMvG6MXHBBPd4JdOX40auhbmKNmBjinPfw96QN9XfV-Q9mc3RhdHVzAA",
"docType": "org.iso.18013.5.1.mDL",
"signature": {
"type": "signature-cose",
"signerKey": {
"type": "jwk",
"jwk": {
"kty": "EC",
"crv": "P-256",
"x": "yfmbtCsdQQ_4EDPg0Gt98gJYk1zkNOpGM9bHYEi49mc",
"y": "MBCAsid4kn7x6KP31bwzRp080D6XlbR2Gxmmc77CKPk"
}
},
"x5cList": [
"MIICKzCCAdGgAwIBAgIURA/oH1EcaR7yH0bVHvdSu95maZYwCgYIKoZIzj0EAwIwLDELMAkGA1UEBhMCQVQxHTAbBgNVBAMMFG1ETCBJc3N1ZXIgSUFDQSBSb290MB4XDTI2MDUxODA2MTkyNloXDTI3MDgxODA2MTkyNlowMjELMAkGA1UEBhMCQVQxIzAhBgNVBAMMGm1ETCBJc3N1ZXIgRG9jdW1lbnQgU2lnbmVyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEyfmbtCsdQQ/4EDPg0Gt98gJYk1zkNOpGM9bHYEi49mcwEICyJ3iSfvHoo/fVvDNGnTzQPpeVtHYbGaZzvsIo+aOByjCBxzAfBgNVHSMEGDAWgBRysvdvSdC8s91wzC1kmaM3gALo4zAdBgNVHQ4EFgQUXGTWuUtFOLFFNFJbOULu+PhBgn0wDgYDVR0PAQH/BAQDAgeAMCcGA1UdEgQgMB6GHGh0dHBzOi8vaWFjYS50ZW5hbnQxLndhbHQuaWQwFQYDVR0lAQH/BAswCQYHKIGMXQUBAjA1BgNVHR8ELjAsMCqgKKAmhiRodHRwczovL2NybC50ZW5hbnQxLndhbHQuaWQvaWFjYS5jcmwwCgYIKoZIzj0EAwIDSAAwRQIgBsfq7TjUHVPTUiTVtCDKQRCdXzp6/eRzqOKH7YaIo9cCIQDreIKTeCNxH5vJCeP4iGFDbaKWuIIGbO1/32bwV/CvOQ=="
]
},
"format": "mso_mdoc",
"mso": {
"version": "1.0",
"digestAlgorithm": "SHA-256",
"valueDigests": {
"org.iso.18013.5.1": {
"0": [
-108,
-117,
118,
36,
45,
-21,
13,
-70,
-21,
-23,
-84,
-41,
-28,
101,
-113,
48,
108,
-122,
-85,
32,
-7,
-127,
57,
-14,
1,
41,
55,
-18,
-112,
-31,
101,
6
],
"1": [
-116,
67,
-77,
-31,
-29,
-104,
-96,
86,
114,
-37,
-68,
43,
101,
74,
-94,
-49,
-107,
-93,
19,
93,
41,
54,
-112,
-70,
50,
-32,
-52,
103,
-41,
29,
-5,
-109
],
"2": [
-24,
-58,
-116,
33,
-63,
-107,
-5,
-39,
-117,
-66,
37,
0,
-42,
87,
1,
107,
127,
20,
55,
67,
127,
-93,
84,
-25,
-35,
94,
96,
-63,
-101,
22,
86,
-99
],
"3": [
113,
-39,
125,
-10,
94,
-31,
-51,
-61,
-123,
13,
-123,
30,
-126,
17,
-105,
-46,
0,
-41,
97,
-120,
4,
55,
-75,
-78,
68,
90,
-32,
73,
33,
85,
-66,
-2
],
"4": [
-118,
-62,
-34,
-121,
-33,
-80,
-60,
43,
-55,
9,
21,
-53,
63,
-4,
8,
-123,
103,
50,
-61,
115,
-18,
6,
60,
82,
-82,
97,
-127,
9,
-50,
-1,
-41,
-43
],
"5": [
-52,
-101,
44,
50,
15,
-117,
113,
-69,
-126,
-114,
-63,
-85,
-120,
32,
-97,
69,
24,
92,
74,
102,
40,
-58,
-77,
-59,
118,
71,
-37,
-103,
-89,
-105,
68,
80
],
"6": [
-95,
-8,
21,
-49,
-80,
-69,
-117,
-116,
-12,
35,
63,
51,
-84,
67,
-31,
52,
20,
76,
-107,
-105,
43,
-32,
72,
106,
64,
36,
118,
-90,
58,
55,
-86,
-54
],
"7": [
-38,
39,
-128,
-20,
-51,
-13,
24,
-124,
-115,
7,
127,
35,
-100,
74,
3,
4,
-125,
31,
-54,
-41,
103,
113,
-33,
-119,
33,
41,
110,
-88,
-124,
-92,
97,
102
],
"8": [
-12,
-60,
-19,
55,
-117,
-79,
117,
67,
73,
127,
101,
-36,
52,
-49,
7,
-116,
-61,
-121,
49,
117,
88,
52,
-26,
11,
13,
-60,
115,
27,
-11,
117,
-29,
73
],
"9": [
-86,
-25,
-122,
-8,
-11,
38,
116,
-1,
44,
37,
-37,
16,
-23,
118,
-18,
7,
36,
51,
1,
-107,
-24,
69,
-36,
1,
-5,
-73,
103,
-46,
28,
109,
-112,
39
]
}
},
"deviceKeyInfo": {
"deviceKey": {
"kty": 1,
"crv": 6,
"x": [
-113,
20,
48,
69,
94,
-19,
29,
-51,
-42,
-4,
-103,
10,
86,
-64,
-39,
47,
-65,
-61,
-76,
-2,
51,
-56,
-46,
109,
67,
-122,
43,
-41,
62,
-2,
49,
-34
]
}
},
"docType": "org.iso.18013.5.1.mDL",
"validityInfo": {
"signed": "2026-05-18T07:48:27.870905006Z",
"validFrom": "2026-05-18T07:48:27.870905006Z",
"validUntil": "2027-05-18T07:48:27.852289522Z"
}
}
}
]
}
}
```
**Key Response Fields:**
- **id**: _String_ - The verification session ID
- **setup**: _Object_ - The complete session configuration that was used to create this verification session (the `VerificationSessionSetup` from the create request)
- **creationDate**: _String_ - ISO 8601 timestamp when the session was created
- **expirationDate**: _String_ - ISO 8601 timestamp when the session expires (default: 10 minutes after creation). Session will expire if left unused (no presentation pushed). Once a presentation is received, the session is no longer eligible for expiry.
- **status**: _String_ - The current status of the verification session. Possible values:
- `"UNKNOWN"` — Session ended up in unknown flow (should be avoided)
- `"ACTIVE"` — Session was created and is active (can be used)
- `"UNUSED"` — Session was not used yet, but not expired (can be used)
- `"IN_USE"` — AuthorizationRequest was requested by wallet
- `"VALIDATING_RECEIVED_REQUEST"` — Checking if received presentation will be processed
- `"PROCESSING_FLOW"` — Received presentation is being processed (validation + policy execution)
- `"EXPIRED"` — Verification request expired without being utilized
- `"SUCCESSFUL"` — Verification completed fully successfully (all validation & policies passed)
- `"FAILED"` — Verification was unsuccessful (presentation validation or policies failed)
- **attempted**: _Boolean_ - Whether the wallet has attempted to respond to the verification request
- **reattemptable**: _Boolean_ - Whether the user can retry this verification session if it failed
- **policies**: _Object_ - The policies configured for this session:
- **vp_policies**: _Object_ (optional) - Policies to run on presentations
- **vc_policies**: _Array_ (optional) - Policies to run on credentials
- **specific_vc_policies**: _Object_ (optional) - Policies to run on specific credential IDs
- **policy_results**: _Object_ (optional) - Detailed results for all policies applied to the verification (only present after presentation is received):
- **vp_policies**: _Object_ - Results for presentation-level policies (audience check, nonce check, envelope signature), organized by credential ID. Structure: `{ "credential_id": { "policy_name": { ... } } }`
- **vc_policies**: _Array_ - Results for credential-level policies (signature, expiry, schema, etc.). Each item contains `policy`, `success`, and `result`.
- **specific_vc_policies**: _Object_ - Results for credential-specific policies
- **overallSuccess**: _Boolean_ (computed) - Whether all policies passed
- **redirects**: _Object_ (optional) - Success and error redirect URIs if configured
**Note:**
Instead of polling the session status, you can use **callbacks** or **Server Sent Events (SSE)** to be notified when the verification completes. See [Callbacks & SSE](https://docs.walt.id/community-stack/verifier2/callback-and-sse.md) for details.
---
## Understanding Flow Types and Core Concepts
Now that you've seen verification in action, let's understand the underlying structure.
### Verification Session Structure
A `VerificationSession` defines the full lifecycle: authorization request → user presentation → policy validation → final result.
Every verification request follows this structure:
```json
{
"flow_type": "cross_device | same_device | dc_api_openid4vp | dc_api_18013_7",
"core_flow": { /* common to all flows */ },
"url_config": { /* specific to cross_device and same_device */ },
"redirects": { /* optional */ }
}
```
**Why this structure?**
- **flow_type** determines how the credential request is delivered (QR code vs deep link vs browser API)
- **core_flow** contains the credential query and policies (same regardless of delivery method)
- **url_config** and **redirects** are flow-specific attributes
This separation makes it easy to switch between flows without changing your credential requirements.
### Flow Type Comparison
| Flow Type | Use Case | Required Attributes | User Experience |
|-----------|----------|---------------------|-----------------|
| `cross_device` | User scans QR code from desktop | None beyond core flow | Desktop shows QR → User scans with phone → Phone wallet opens |
| `same_device` | User clicks link on mobile | `redirects` (success/error URLs) | Browser redirects to wallet → Wallet opens → User returns to browser |
| `dc_api_openid4vp` | Browser-native OpenID4VP credential exchange | `expectedOrigins` (array of allowed origins) | Browser shows native credential picker → User selects → Instant return |
| `dc_api_18013_7` | Browser-native ISO 18013-7 credential exchange | `expectedOrigins` with exactly one origin and `core_flow.requestedElements` | Browser shows native credential picker → User selects → Instant return |
---
## Configuration Defaults
Many session parameters can be configured globally in `verifier-service.conf` and overridden per-request when needed.
| Parameter | Config File Location | Request Override Path | When to Override |
|-----------|---------------------|----------------------|------------------|
| `urlPrefix` | `verifier-service.conf` → `urlPrefix` | `url_config.url_prefix` | Testing with ngrok, different environments per session |
| `urlHost` | `verifier-service.conf` → `urlHost` | `url_config.url_host` | Custom authorization endpoints, conformance testing |
| `clientId` | `verifier-service.conf` → `clientId` | `core_flow.clientId` | Different verifier identities per use case |
| `clientMetadata` | `verifier-service.conf` → `clientMetadata` | `core_flow.client_metadata` | Session-specific branding, multi-tenant scenarios |
| Signing key | `verifier-service.conf` → `key` | `core_flow.key` | Different signing keys per session (advanced) |
| Certificate chain | `verifier-service.conf` → `x5c` | `core_flow.x5c` | Different certificate chains per session (x509 flows) |
**Note:**
**Best Practice:** Set defaults in `verifier-service.conf` for your primary use case, and override per-request only when necessary. This keeps your API calls clean and reduces duplication.
See [Verifier Service Configuration](https://docs.walt.id/community-stack/verifier2/configurations/config-files/verifier-service.md) for detailed configuration documentation.
## Additional
You can find more information on how to configure different DC API request flows, and how to interact with the Browser-based DC API in our [Digital Credential Button project](https://github.com/walt-id/waltid-identity/tree/main/waltid-applications/waltid-digital-credentials).
## Next Steps
- **Apply custom policies** — See [Policies](https://docs.walt.id/community-stack/verifier2/policies/overview.md) for validation rules beyond signature verification
- **Set up notifications** — Use [Callbacks & SSE](https://docs.walt.id/community-stack/verifier2/callback-and-sse.md) instead of polling for session status
- **Verify other credential types** — Try [W3C VC](https://docs.walt.id/community-stack/verifier2/credential-verification/vc-oid4vp.md) or [SD-JWT VC](https://docs.walt.id/community-stack/verifier2/credential-verification/sd-jwt-vc-oid4vp.md)
--------------------------------------------------------------------------------
PAGE: Overview
URL: https://docs.walt.id/community-stack/verifier2/policies/overview
RAW: https://docs.walt.id/community-stack/verifier2/policies/overview.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Policies
Verifier 2 introduces a fully parallelized validation process:
- [Credential (VC) validation](https://docs.walt.id/community-stack/verifier2/policies/available-policies.md#available-policies-vc)
- [Presentation (VP) validation](https://docs.walt.id/community-stack/verifier2/policies/available-policies.md#available-policies-vp)
- Specific VC-Type Policies (if configured)
Instead of sequential execution, VC-, VP-, and type-specific policies are executed concurrently, significantly improving performance for multi-credential presentations.
Each of them can be defined in the policies object as shown below:
```json
"policies": {
"vp_policies": [...],
"vc_policies": [...],
"vc_type_policies": {
"VerifiableID": [...],
"BankStatementCredential": [...]
}
}
```
Learn more about the policy configuration [here](https://docs.walt.id/community-stack/verifier2/policies/configuration.md).
--------------------------------------------------------------------------------
PAGE: Policy Configuration Levels
URL: https://docs.walt.id/community-stack/verifier2/policies/configuration
RAW: https://docs.walt.id/community-stack/verifier2/policies/configuration.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Policy Configuration Levels
The verification policy system supports multiple levels of policy configuration, allowing fine-grained control over which policies are applied to which credentials and presentations. This document explains how policies can be configured at different scopes based on the implementation in the verifier libraries.
## Policy Configuration Structure
Policies are organized into a hierarchical structure
```kotlin
data class DefinedVerificationPolicies(
/** Policies to run on the presentations (VP-level) */
val vp_policies: VPPolicyList? = null,
/** Policies to run on all credentials (Global VC-level) */
val vc_policies: VCPolicyList? = null,
/** Policies to run on specific credential query IDs (Specific VC-level) */
val specific_vc_policies: Map? = null
)
```
## Policy Levels Explained
### 1. VP-Level Policies (`vp_policies`)
**Scope:** Applied to Verifiable Presentations themselves, before credential extraction.
**Purpose:** Validate presentation-level properties such as:
- Presentation signature (holder binding)
- Audience matching
- Nonce validation
- Presentation format-specific checks (e.g., SD hash for SD-JWT, device auth for mdoc)
**When Applied:**
- First in the verification flow
- Applied to each presentation in the `vp_token` response
- Format-specific: policies are organized by presentation format (`jwt_vc_json`, `dc+sd-jwt`, `mso_mdoc`)
**Example Configuration:**
```json
{
"vp_policies": {
"jwt_vc_json": [
"jwt_vc_json/audience-check",
"jwt_vc_json/nonce-check",
"jwt_vc_json/envelope_signature"
],
"dc+sd-jwt": [
"dc+sd-jwt/audience-check",
"dc+sd-jwt/nonce-check",
"dc+sd-jwt/kb-jwt_signature",
"dc+sd-jwt/sd_hash-check"
],
"mso_mdoc": [
"mso_mdoc/device-auth",
"mso_mdoc/issuer_auth",
"mso_mdoc/issuer_signed_integrity",
"mso_mdoc/mso"
]
}
}
```
**Default Behavior:** If not specified, default policies are applied based on the presentation format:
- `jwt_vc_json`: audience-check, nonce-check, envelope_signature
- `dc+sd-jwt`: audience-check, nonce-check, kb-jwt_signature, sd_hash-check
- `mso_mdoc`: device-auth, device_key_auth, issuer_auth, issuer_signed_integrity, mso
**Available Policies:**
- [Available Policies (VP)](https://docs.walt.id/community-stack/verifier2/policies/available-policies.md#available-policies-vp)
---
### 2. Global VC-Level Policies (`vc_policies`)
**Scope:** Applied to **all credentials** extracted from all presentations, regardless of which query they satisfy.
**Purpose:** Enforce universal credential requirements such as:
- Signature verification
- Expiration checks
- Not-before validation
- Issuer allowlists
- Schema validation
- Webhook verification
**When Applied:**
- After presentations are validated and credentials are extracted
- Applied to every credential in parallel
- Runs regardless of which DCQL query the credential satisfies
**Example Configuration:**
```json
{
"vc_policies": [
"signature",
"expiration",
"not-before",
{
"policy": "allowed-issuer",
"allowed_issuer": ["did:example:trusted-issuer"]
},
{
"policy": "schema",
"schema": {
"type": "object",
"properties": {
"credentialSubject": {
"type": "object"
}
}
}
}
]
}
```
**Default Behavior:** If not specified, defaults to `["signature"]` (signature verification only).
**Available Policies:**
- [Available Policies (VC)](https://docs.walt.id/community-stack/verifier2/policies/available-policies.md#available-policies-vc)
---
### 3. Specific VC-Level Policies (`specific_vc_policies`)
**Scope:** Applied to credentials that satisfy **specific DCQL credential queries**, identified by query ID.
**Purpose:** Enforce credential-type-specific or query-specific requirements such as:
- Schema validation for specific credential types
- Issuer restrictions for specific credential queries
- Custom validation rules for particular credential formats
- Field-specific regex patterns
**When Applied:**
- After global VC policies
- Only to credentials that match the specified query ID
- Allows different policies for different credential types in the same session
**Example Configuration:**
```json
{
"specific_vc_policies": {
"identity_credential": [
{
"policy": "schema",
"schema": {
"type": "object",
"required": ["credentialSubject"],
"properties": {
"credentialSubject": {
"type": "object",
"required": ["name", "email"]
}
}
}
},
{
"policy": "allowed-issuer",
"allowed_issuer": ["did:example:identity-issuer"]
}
],
"drivers_license": [
{
"policy": "vical",
"vical": "",
"enableDocumentTypeValidation": true,
"enableRevocation": true
}
],
"age_verification": [
{
"policy": "regex",
"path": "$.credentialSubject.age",
"regex": "^[0-9]{1,3}$"
}
]
}
}
```
**Available Policies:**
- [Available Policies (Specific VC)](https://docs.walt.id/community-stack/verifier2/policies/available-policies.md#available-policies-specific-vc)
---
## Policy Execution Flow
The verification engine executes policies in the following order:
```
1. Parse vp_token → Extract presentations
↓
2. VP-Level Policies (vp_policies)
- Validate presentation signatures, audience, nonce
- Format-specific checks (SD hash, device auth, etc.)
↓
3. Extract credentials from validated presentations
↓
4. Global VC-Level Policies (vc_policies)
- Applied to ALL credentials in parallel
↓
5. Specific VC-Level Policies (specific_vc_policies)
- Applied to credentials matching specific query IDs
↓
6. Aggregate results and determine overall success
```
**Key Points:**
- Policies at each level run **in parallel** (using coroutines)
- If any policy fails, the overall verification may fail (depending on configuration)
- Results are collected and aggregated into `Verifier2PolicyResults`
- The `overallSuccess` flag requires all policies at all levels to succeed
---
## Complete Example
Here's a complete example showing all policy levels:
```json
{
"dcql_query": {
"credentials": [
{
"id": "identity_credential",
"format": "jwt_vc_json",
"claims": [...]
},
{
"id": "drivers_license",
"format": "mso_mdoc",
"meta": {
"doctype_value": "org.iso.18013.5.1.mDL"
}
}
]
},
"policies": {
"vp_policies": {
"jwt_vc_json": [
"jwt_vc_json/audience-check",
"jwt_vc_json/nonce-check",
"jwt_vc_json/envelope_signature"
],
"mso_mdoc": [
"mso_mdoc/device-auth",
"mso_mdoc/issuer_auth",
"mso_mdoc/mso"
]
},
"vc_policies": [
"signature",
"expiration",
"not-before",
{
"policy": "allowed-issuer",
"allowed_issuer": [
"did:example:trusted-issuer-1",
"did:example:trusted-issuer-2"
]
}
],
"specific_vc_policies": {
"identity_credential": [
{
"policy": "schema",
"schema": {
"type": "object",
"required": ["credentialSubject"],
"properties": {
"credentialSubject": {
"type": "object",
"required": ["name", "email"]
}
}
}
},
{
"policy": "regex",
"path": "$.credentialSubject.email",
"regex": "^.+@example\.com$"
}
],
"drivers_license": [
{
"policy": "vical",
"vical": "",
"enableDocumentTypeValidation": true,
"enableRevocation": true
}
]
}
}
}
```
**What happens:**
1. **VP Policies:**
- JWT VC JSON presentations are validated for audience, nonce, and signature
- mdoc presentations are validated for device auth, issuer auth, and MSO validity
2. **Global VC Policies:**
- ALL credentials (both identity and drivers license) are checked for:
- Signature validity
- Expiration status
- Not-before validity
- Issuer is in the trusted list
3. **Specific VC Policies:**
- Credentials matching `identity_credential` query additionally checked for:
- Schema compliance (must have credentialSubject with name and email)
- Email format matches `@example.com` pattern
- Credentials matching `drivers_license` query additionally checked for:
- VICAL trust anchor validation
- Document type validation
- Revocation status
---
## Policy Result Structure
Results are collected into `Verifier2PolicyResults`:
```kotlin
data class Verifier2PolicyResults(
val vpPolicies: Map>,
val vcPolicies: List,
val specificVcPolicies: Map>
) {
val overallSuccess: Boolean =
vpPolicies.all { it.value.all { it.value.success } } &&
vcPolicies.all { it.success } &&
specificVcPolicies.values.all { policies -> policies.all { it.success } }
}
```
The `overallSuccess` property requires **all policies at all levels** to succeed for the verification to be considered successful.
---
## Use Cases
### Use Case 1: Basic Verification
Apply only essential checks to all credentials:
```json
{
"vc_policies": ["signature", "expiration"]
}
```
### Use Case 2: Format-Specific Requirements
Different policies for different credential types:
```json
{
"vc_policies": ["signature"],
"specific_vc_policies": {
"mdoc_credential": [
{"policy": "vical", "vical": "..."}
],
"jwt_credential": [
{"policy": "schema", "schema": {...}}
]
}
}
```
### Use Case 3: Strict Verification
Comprehensive checks at all levels:
```json
{
"vp_policies": {...}, // All presentation checks
"vc_policies": [
"signature",
"expiration",
"not-before",
{"policy": "allowed-issuer", "allowed_issuer": [...]}
],
"specific_vc_policies": {
"each_query": [
{"policy": "schema", "schema": {...}}
]
}
}
```
---
## Configuration via API
When creating a verification session via the API (`POST /verification-session/create`), policies are specified in the `policies` field of `VerificationSessionSetup`:
```json
{
"flow_type": "cross_device",
"core_flow": {
"dcql_query": {...},
"policies": {
"vp_policies": {...},
"vc_policies": [...],
"specific_vc_policies": {...}
}
}
}
```
If policies are not specified, defaults are applied:
- **VP policies:** Format-specific defaults (see VP-Level Policies section)
- **VC policies:** `["signature"]` (signature verification only)
- **Specific VC policies:** `{}` (empty, no specific policies)
--------------------------------------------------------------------------------
PAGE: Available Policies
URL: https://docs.walt.id/community-stack/verifier2/policies/available-policies
RAW: https://docs.walt.id/community-stack/verifier2/policies/available-policies.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Available Policies
## Available Policies (VC)
### `expiration`
Checks if a credential has expired based on its `notAfter` claim.
**Use case:** Ensure credentials are still valid and haven't expired.
**Example:**
```json
{
"policy": "expiration"
}
```
---
### `not-before`
Validates that a credential is not being used before its `notBefore` or `issuedAt` date.
**Use case:** Prevent premature use of credentials that have future validity dates.
**Example:**
```json
{
"policy": "not-before"
}
```
---
### `schema`
Validates credential data against a JSON Schema.
**Use case:** Ensure credential structure and data types match expected schema.
**Example:**
```json
{
"policy": "schema",
"schema": {
"type": "object",
"properties": {
"credentialSubject": {
"type": "object",
"properties": {
"name": { "type": "string" }
}
}
}
},
"defaultType": null
}
```
---
### `signature`
Verifies the cryptographic signature of a credential.
**Use case:** Ensure credential authenticity and integrity.
**Example:**
```json
{
"policy": "signature"
}
```
---
### `allowed-issuer`
Validates that the credential issuer is in an allowed list.
**Use case:** Restrict credentials to trusted issuers only.
**Example:**
```json
{
"policy": "allowed-issuer",
"allowed_issuer": "did:example:issuer123"
}
```
**Multiple issuers:**
```json
{
"policy": "allowed-issuer",
"allowed_issuer": ["did:example:issuer1", "did:example:issuer2"]
}
```
---
### `regex`
Validates credential data at a specific path using a regular expression.
**Use case:** Pattern matching for specific credential fields (e.g., email format, ID numbers).
**Example:**
```json
{
"policy": "regex",
"path": "$.credentialSubject.email",
"regex": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
"regex_options": ["IGNORE_CASE"],
"allowNull": false
}
```
---
### `credential-status`
Checks credential status for different status list formats.
**Use case:** Verify credential hasn't been revoked or suspended.
**Example (W3C StatusList2021):**
```json
{
"policy": "credential-status",
"argument": {
"discriminator": "w3c",
"value": 0,
"purpose": "revocation",
"type": "StatusList2021"
}
}
```
**Example (TokenStatusList):**
```json
{
"policy": "credential-status",
"argument": {
"discriminator": "ietf",
"value": 0
}
}
```
**Example (Multiple allowed status values):**
You can specify multiple allowed status values using the `values` field. This is useful when you want to accept credentials that are in any of several valid states.
```json
{
"policy": "credential-status",
"argument": {
"discriminator": "ietf",
"values": [0, 1]
}
}
```
```json
{
"policy": "credential-status",
"argument": {
"discriminator": "w3c",
"values": [0, 1],
"purpose": "revocation",
"type": "BitstringStatusList"
}
}
```
**Example (Multiple BitstringStatusList entries):**
```json
{
"policy": "credential-status",
"argument": {
"discriminator": "w3c-list",
"list" : [
{
"value": 0,
"purpose": "revocation",
"type": "BitstringStatusList"
},
{
"value": 0,
"purpose": "suspension",
"type": "BitstringStatusList"
}
]
}
}
```
**Note:**
**Note:** You can use either `value` (single value) or `values` (array of values), but at least one must be provided. When using `values`, the credential status is considered valid if it matches any of the specified values.
---
### `revoked-status-list`
Checks revocation status using W3C StatusList2021 format.
**Use case:** Quick revocation check for StatusList2021 credentials.
**Example:**
```json
{
"policy": "revoked-status-list"
}
```
---
### `vical`
Validates mdoc credentials using VICAL (Verified Issuer Certificate Authority List) data.
**Use case:** Verify mdoc/mDL authenticity and certificate chain using trusted VICAL data. This policy validates the credential's X.509 certificate chain against a list of trusted Issuing Authority Certificate Authorities (IACAs).
**Note:**
**Background:** VICAL is part of the ISO/IEC 18013-5 standard trust infrastructure for mobile documents. It provides a cryptographically signed list of trusted issuer certificate authorities. Learn more about [VICALs and IACAs in the concepts section](https://docs.walt.id/concepts/trust-systems/iso-trust-vicals.md).
**Info:**
**mdoc/mDL Only:** This policy can currently only be applied to mdoc credentials (ISO 18013-5 format). It will fail if applied to other credential formats like JWT or SD-JWT.
#### Configuration Parameters
The VICAL policy requires either `vical` or `vicalUrl` (exactly one must be provided):
**VICAL Source (choose one):**
- `vical`: _String_ - Base64-encoded VICAL CBOR data. Use this for inline trust lists embedded directly in the policy configuration.
- `vicalUrl`: _String (optional)_ - HTTP(S) URL from which to fetch the VICAL at verification time. The endpoint must return the raw VICAL CBOR bytes (either as binary `application/cbor` or hex-encoded text). This is the **preferred approach in production** as it decouples policy configuration from the trust list and allows updates without redeploying configuration.
**Validation Options:**
- `enableDocumentTypeValidation`: _Boolean_ - When `true`, filters VICAL trust anchors to only those that authorize the specific document type (e.g., `org.iso.18013.5.1.mDL`). When `false`, uses all trust anchors from the VICAL regardless of document type. Default: `false`.
- `enableTrustedChainRoot`: _Boolean_ - When `true`, requires the certificate chain to include a trusted self-signed root certificate. When `false`, chain validation is more lenient. Default: `false`.
- `enableSystemTrustAnchors`: _Boolean_ - When `true`, uses the system's built-in trust anchors (operating system certificate stores) in addition to VICAL trust anchors. When `false`, relies only on VICAL trust anchors. Default: `false`.
- `enableRevocation`: _Boolean_ - When `true`, performs revocation checks on certificates in the chain (requires network access to CRL/OCSP endpoints). When `false`, skips revocation checking. Default: `false`.
#### Example: Inline VICAL (Base64)
```json
{
"policy": "vical",
"vical": "o2d2ZXJzaW9uAWx2aWNhbERhdGGCpmhkb2NUeXBlgXhfb3JnLmlzby4xODAxMy41LjEubURMamNlcnRpZmljYXRlWQOuMIID...",
"enableDocumentTypeValidation": true,
"enableTrustedChainRoot": true
}
```
**Note:**
If you have a VICAL CBOR file, encode it using: `base64 < austroads.cbor`. Remove any newlines from the output before using it in JSON.
#### Example: VICAL URL (Recommended)
```json
{
"policy": "vical",
"vicalUrl": "https://vical.example.com/api/latest",
"enableDocumentTypeValidation": true,
"enableTrustedChainRoot": true,
"enableSystemTrustAnchors": false,
"enableRevocation": false
}
```
**Note:**
**Enterprise Stack:** If you're using the walt.id Enterprise Stack, the [VICAL Service](https://docs.walt.id/enterprise-stack/services/vical-service/overview.md) provides managed VICAL endpoints. Use the `/v1/{target}/vical-service-api/latest` endpoint as your `vicalUrl`.
---
### `webhook`
Calls an external HTTP endpoint to verify the credential.
**Use case:** Delegate verification to external systems or custom validation logic.
**Example:**
```json
{
"policy": "webhook",
"url": "https://api.example.com/verify",
"basicauth_username": "user",
"basicauth_password": "pass",
"bearerauth_token": null
}
```
**Bearer token authentication:**
```json
{
"policy": "webhook",
"url": "https://api.example.com/verify",
"bearerauth_token": "your-token-here"
}
```
## Available Policies (VP)
Policies are organized by presentation format. All policies can be referenced by their ID string or configured as JSON objects.
### JWT VC JSON Policies
#### `jwt_vc_json/audience-check`
Validates that the presentation audience matches the expected audience for the verification session.
**Use case:** Ensure the presentation is intended for the correct verifier.
**Example:**
```json
{
"policy": "jwt_vc_json/audience-check"
}
```
---
#### `jwt_vc_json/nonce-check`
Validates that the presentation nonce matches the expected nonce for the verification session.
**Use case:** Prevent replay attacks by ensuring the presentation was created for this specific session.
**Example:**
```json
{
"policy": "jwt_vc_json/nonce-check"
}
```
---
#### `jwt_vc_json/envelope_signature`
Verifies the presentation envelope signature using the holder's public key.
**Use case:** Ensure the presentation was signed by the holder and hasn't been tampered with.
**Example:**
```json
{
"policy": "jwt_vc_json/envelope_signature"
}
```
---
### SD-JWT Policies
#### `dc+sd-jwt/audience-check`
Validates that the SD-JWT presentation audience matches the expected audience for the verification session.
**Use case:** Ensure the SD-JWT presentation is intended for the correct verifier.
**Example:**
```json
{
"policy": "dc+sd-jwt/audience-check"
}
```
---
#### `dc+sd-jwt/nonce-check`
Validates that the SD-JWT presentation nonce matches the expected nonce for the verification session.
**Use case:** Prevent replay attacks by ensuring the presentation was created for this specific session.
**Example:**
```json
{
"policy": "dc+sd-jwt/nonce-check"
}
```
---
#### `dc+sd-jwt/kb-jwt_signature`
Verifies the Key Binding JWT (KB-JWT) signature using the holder's public key.
**Use case:** Ensure the KB-JWT was signed by the holder and binds the presentation to the holder's key.
**Example:**
```json
{
"policy": "dc+sd-jwt/kb-jwt_signature"
}
```
---
#### `dc+sd-jwt/sd_hash-check`
Verifies SD-JWT key binding by recalculating and comparing the SD hash.
**Use case:** Ensure the presentation hash matches the expected hash, validating key binding integrity.
**Example:**
```json
{
"policy": "dc+sd-jwt/sd_hash-check"
}
```
---
### mdoc Policies
#### `mso_mdoc/device-auth`
Verifies device authentication using device signature or MAC.
**Use case:** Ensure the mdoc presentation was authenticated by the device that holds the credential.
**Example:**
```json
{
"policy": "mso_mdoc/device-auth"
}
```
---
#### `mso_mdoc/device_key_auth`
Verifies holder-verified data authorization using KeyAuthorization from the MSO.
**Use case:** Ensure holder-verified data elements are authorized according to the issuer's KeyAuthorization.
**Example:**
```json
{
"policy": "mso_mdoc/device_key_auth"
}
```
---
#### `mso_mdoc/issuer_auth`
Verifies issuer authentication by validating the COSE_Sign1 signature using the issuer's certificate chain.
**Use case:** Ensure the issuer-signed data was authenticated by the credential issuer.
**Example:**
```json
{
"policy": "mso_mdoc/issuer_auth"
}
```
---
#### `mso_mdoc/issuer_signed_integrity`
Verifies issuer-signed data integrity by comparing value digests with the MSO.
**Use case:** Ensure issuer-signed data hasn't been tampered with by validating digest hashes.
**Example:**
```json
{
"policy": "mso_mdoc/issuer_signed_integrity"
}
```
---
#### `mso_mdoc/mso`
Verifies the Mobile Security Object (MSO) validity, including timestamps and digest algorithm support.
**Use case:** Ensure the MSO is valid and within its validity period.
**Example:**
```json
{
"policy": "mso_mdoc/mso"
}
```
--------------------------------------------------------------------------------
PAGE: Callback and SSE
URL: https://docs.walt.id/community-stack/verifier2/callback-and-sse
RAW: https://docs.walt.id/community-stack/verifier2/callback-and-sse.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Callback and SSE
The verifier service can be configured to use a callback or Server Sent Events (SSE) to be notified when the verification session is completed.
## Callback
The callback is a URL that will be called when the verification session is completed. The callback will be called with the verification session ID and the verification session object.
**Note:**
In previous versions of the verifier service, the callback information was passed through headers. This is no longer supported and the required configuration is now passed in the body of the request.
Example Request:
```json
"core_flow": {
"dcql_query": {
"credentials": [
{
"id": "example_openbadge_jwt_vc",
"format": "jwt_vc_json",
"meta": {
"type_values": [
[
"VerifiableCredential",
"OpenBadgeCredential"
]
]
},
"claims": [
{
"path": [
"name"
]
}
]
}
]
},
"webhook": {
"url": "https://example.com/callback",
"basicAuthUser": "1234567890",
"basicAuthPass": "1234567890",
"bearerToken": "1234567890"
}
}
```
**Body Parameters**
- **dcql_query**: The DCQL query to be used for the verification.
- **webhook**: The webhook configuration to be used for the verification.
- **url**: The URL to be called when the verification session is completed.
For authentication, you can use basic authentication or bearer token authentication.
- **basicAuthUser**: The username for basic authentication.
- **basicAuthPass**: The password for basic authentication.
OR
- **bearerToken**: The bearer token for bearer authentication.
## Server Sent Events (SSE)
The SSE is a stream of events that will be sent to the client when the verification session is completed. The events will be sent to the client via a WebSocket connection.
From the desired client, you can subscribe to the SSE stream by using the following endpoint:
```bash
curl -X 'GET' \
'https://{orgID}.enterprise-sandbox.waltid.dev/v2/{verificationSessionID}/verifier-service-api/verification-session/events' \
-H 'Authorization: Bearer {yourToken}'
```
**Path Parameters**
- **orgID**: The organization ID.
- **verificationSessionID**: The target indicates the verification session to target, generally given as {target}.{sessionID} (e.g. org.tenant.verifier.61566cec-23ea-4fca-9049-5c8b316e3271) which is returned in the `creationTarget` parameter when creating the verification session.
## Verification Session Info
As is the case with polling the verification session info endpoint, both the callback andthe SSE stream will return the verficiation session object containing the following useful fields:
- `status`: The status of the verification session.
- `attempted`: Whether the verification session has been attempted.
- `reattemptable`: Whether the verification session can be retried.
- `policyResults`: The results of the policies applied to the verification session.
- `presentedRawData`: The raw data verifiable presentation token.
- `presentedCredentials`: The parsed credentials presented by the user.
- `failure`: Structured detail of *why* a session ended in `FAILED` (see below). `null` for successful or in-progress sessions.
Policy results will be split into vcPolicy and vpPolicy results. More information about the policy results can be found [here](https://docs.walt.id/community-stack/verifier2/policies/overview.md).
## Structured failure detail
When `status` is `FAILED`, the session carries a `failure` object describing the cause. The `type` discriminator distinguishes the failure modes, and each subtype carries detail specific to that mode. `statusReason` may contain a human-readable summary for quick display; use `failure.reason` for the canonical failure explanation.
All existing webhook fields keep their shape — `failure` is a new optional field. Consumers that only read `status` / `statusReason` / `policyResults` keep working unchanged.
### VC policy violation (per-credential attribution)
When a `vc_policies` check fails, each affected policy result carries `query_id` + `credential_index` so the failure can be tied back to the specific credential that failed. The same attributed policy result entries are exposed in `failure.violations`.
```json
{
"event": "credential_policy_results_available",
"session": {
"status": "FAILED",
"policy_results": {
"vc_policies": [
{
"query_id": "pid",
"credential_index": 1,
"policy": {"id": "signature"},
"success": false,
"error": "Signature verification failed"
}
]
},
"failure": {
"type": "vc_policy_violations",
"reason": "1 credential policy violation(s)",
"violations": [
{
"query_id": "pid",
"credential_index": 1,
"policy": {"id": "signature"},
"success": false,
"error": "Signature verification failed"
}
]
}
}
}
```
### DCQL fulfillment failure
When the presented credentials do not satisfy the DCQL query (missing required IDs or unsatisfied required `credential_set` options), `failure.failure` lists exactly which queries or sets were unmet.
```json
{
"event": "dcql_fulfillment_check_failed",
"session": {
"status": "FAILED",
"failure": {
"type": "dcql_fulfillment",
"reason": "DCQL Fulfillment Failed: 1 required CredentialSet(s) not satisfied. Unsatisfied sets: [[[pid], [mdl]]], Successfully validated query IDs: [email]",
"failure": {
"missing_query_ids": [],
"unsatisfied_sets": [
{ "options": [["pid"], ["mdl"]] }
],
"successfully_validated_query_ids": ["email"]
}
}
}
}
```
### Presentation validation failure
When one or more VP policies fail (e.g. bad signature, wrong audience), `failure.failed_policies` mirrors `presentation_validation_results` but filtered to only the failed entries.
```json
{
"event": "presentation_validation_failed",
"session": {
"status": "FAILED",
"failure": {
"type": "presentation_validation",
"reason": "Presentation validation failed: ...",
"failed_policies": {
"pid": {
"jwt_vc_json/envelope_signature": {
"success": false,
"errors": [ { "error": "InvalidSignature", "message": "..." } ]
}
}
}
}
}
}
```
### Wallet-initiated rejection (OpenID4VP §8.5)
Wallets can POST an OpenID4VP [§8.5 error response](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#section-8.5) to the `direct_post` endpoint instead of a `vp_token` — for example when the user refuses to share credentials. `WalletPresentFunctionality2.walletRejectHandling` can create these rejection responses, and the verifier accepts both url-encoded (`error=access_denied&error_description=…&state=…`) and JSON bodies. When the original request included `state`, the error response must echo the same `state`.
Session transitions to `FAILED` with `failure.type = "wallet_error_response"`, and the webhook emits the new `wallet_error_response_received` event. If the verifier session was configured with `redirects.errorRedirectUri`, the `direct_post` response contains that URL as `redirect_uri` so the wallet can send the user back to the verifier's error page.
```json
{
"event": "wallet_error_response_received",
"session": {
"status": "FAILED",
"statusReason": "Wallet returned OID4VP error: access_denied",
"failure": {
"type": "wallet_error_response",
"reason": "Wallet returned OID4VP error response per §8.5",
"error": "access_denied",
"error_description": "User denied",
"state": "abc123"
}
}
}
```
**State echo**: when the original Authorization Request included a `state`, the wallet error response **must** echo the same `state`. Missing or mismatched state is rejected with `invalid_request` (HTTP 400) — matching the OAuth 2.0 / §8.5 contract.
**Idempotency**: once the session has reached a terminal state (`SUCCESSFUL` or `FAILED`), a duplicate or late wallet error POST is acknowledged with HTTP 200 but does **not** overwrite the existing `status` / `statusReason` / `failure`. This prevents a replayed or malicious error POST from flipping a successful verification to failed.
**Wallet helper**: use `WalletPresentFunctionality2.walletRejectHandling(...)` to produce the rejection response on the wallet side. The helper supports `fragment`, `query`, `form_post`, `direct_post`, and `direct_post.jwt` response modes, and accepts either the typed `OID4VPErrorCode` enum (recommended) or a raw `String` error code for forward compatibility with codes not yet in the enum. For `direct_post.jwt`, the helper sends the unencrypted form-encoded error response to `response_uri`, which OpenID4VP 1.0 permits when the Wallet cannot generate an encrypted response.
--------------------------------------------------------------------------------
PAGE: Feature Manager
URL: https://docs.walt.id/community-stack/verifier2/configurations/feature-manager
RAW: https://docs.walt.id/community-stack/verifier2/configurations/feature-manager.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Feature Manager
Use the feature manager to control which Verifier2 API features are enabled at service startup.
This reduces complexity and the risk of misconfigurations.
## Feature Types
The feature manager offers two types: **Base** and **Optional**.
### Base Features
Base features are always enabled and do not require additional configuration in most cases. They provide
critical functionality expected to be available by default.
### Optional Features
Optional features can be enabled or disabled based on your requirements.
1. **Enabled by default**: These features work without additional configuration (e.g. no extra config file needed).
2. **Disabled by default**: These features must be explicitly enabled in `_features.conf`. They may require their
own configuration to function.
**Note:**
Each feature may have zero, one, or multiple configuration files associated with it.
## How the Feature Manager Works
The Feature Manager ensures efficient and error-free service initialisation through the following steps:
1. **Initialisation with Feature Catalogs**: Services are passed one or more Feature Catalogs on startup.
2. **Determination of Active Features**: The initialiser identifies explicitly enabled or disabled features and
resolves the default states of optional features.
3. **Loading Configurations**: Configurations for base features and explicitly enabled optional features are loaded.
**This results in:**
- Only necessary configurations being loaded, reducing complexity and avoiding unused features.
- Minimal required configuration through sensible defaults.
- Invalid configurations being caught during initialisation before they can reach production.
## Feature Configuration
Configure features by updating the `_features.conf` file as
explained [here](https://docs.walt.id/community-stack/verifier2/configurations/config-files/features.md).
--------------------------------------------------------------------------------
PAGE: Features
URL: https://docs.walt.id/community-stack/verifier2/configurations/config-files/features
RAW: https://docs.walt.id/community-stack/verifier2/configurations/config-files/features.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Features
With `_features.conf` you can control which Verifier2 API features are enabled. They are managed via
the [feature manager](https://docs.walt.id/community-stack/verifier2/configurations/feature-manager.md).
## Available Features
### Base Features
Base features are always enabled. They do not need to be listed in `_features.conf`.
- `verifier-service` — Core verification service implementation. Handles verification session creation,
authorization request generation, and VP response processing.
### Optional Features
- `dev-mode` — Development mode. Disabled by default. When enabled, loads additional settings from
`dev-mode.conf` (e.g. allows DID Web resolution over plain HTTP). See
[dev-mode.conf](https://docs.walt.id/community-stack/verifier2/configurations/config-files/dev-mode.md) for details.
- `transaction-data-profiles` — Transaction-data type registry and discovery endpoint. Enabled by default. Loads
`transaction-data-profiles.conf` and exposes `GET /transaction-data-profiles`. See
[transaction-data-profiles.conf](https://docs.walt.id/community-stack/verifier2/configurations/config-files/transaction-data-profiles.md) for details.
## Example Setup
```editorconfig[_features.conf]
enabledFeatures = [
dev-mode
]
disabledFeatures = [
# ...
]
```
--------------------------------------------------------------------------------
PAGE: Web
URL: https://docs.walt.id/community-stack/verifier2/configurations/config-files/web
RAW: https://docs.walt.id/community-stack/verifier2/configurations/config-files/web.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Web
With `web.conf` you can configure the host and port the Verifier2 API binds to for local network connections.
### `webHost`
_String — default: `"0.0.0.0"`_
The network interface the service binds to. `0.0.0.0` listens on all available interfaces.
### `webPort`
_String — default: `"7004"`_
The port the service listens on.
**Note:**
Verifier2 listens on port `7004` by default, whether run standalone or via Docker Compose — this avoids a conflict with the original Verifier (v1), which uses `7003`.
## Example
```editorconfig[web.conf]
webHost = "0.0.0.0"
webPort = "7004"
```
--------------------------------------------------------------------------------
PAGE: Verifier Service
URL: https://docs.walt.id/community-stack/verifier2/configurations/config-files/verifier-service
RAW: https://docs.walt.id/community-stack/verifier2/configurations/config-files/verifier-service.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Verifier Service
`verifier-service.conf` configures the service-level defaults for the Verifier2 API — its identity as presented
to wallets, the URLs used in authorization requests, and the optional signing key and certificate chain.
**Note:**
All fields in this file act as **defaults**. Every value can be overridden per-request in the request body when
creating a verification session.
## Fields
### `clientId`
_String_ — The identifier the verifier presents to wallets during the credential exchange.
The following formats are supported (defined in OID4VP 1.0, §5.9.3):
- **Plain string** — e.g. `"verifier2"`: used as-is in the authorization request. Suitable for development and
use cases where the verifier does not need a verifiable identity.
- **`x509_san_dns:`** — e.g. `"x509_san_dns:verifier.example.com"`: the client identity is bound to
the DNS name in the certificate's Subject Alternative Name extension. Wallets verify the signed request against
the SAN in the provided certificate. Use together with `key` and `x5c`.
- **`x509_hash:`** — the client identity is bound to the SHA-256 hash of the
DER-encoded leaf certificate. Wallets verify the signed request and check that the certificate matches the
hash. Use together with `key` and `x5c`. Required by the HAIP profile.
Both `x509_san_dns` and `x509_hash` are general OID4VP client identifier schemes and can be used with any
signed OID4VP request (cross-device, same-device, or DC API).
For unsigned DC API flows (`signed_request: false`), `clientId` is automatically omitted from the authorization
request regardless of this setting.
### `clientMetadata`
_Object_ — Metadata about the verifier that wallets may display to users during the presentation flow.
Corresponds to the `client_metadata` parameter defined in OID4VP and [RFC 7591](https://www.rfc-editor.org/rfc/rfc7591).
All fields within `clientMetadata` are optional. Fields that support internationalisation can have
language-tagged variants using the format `"field_name#language-tag"` (e.g. `"client_name#fr-FR"`).
Language tags follow [BCP 47](https://www.rfc-editor.org/rfc/bcp/bcp47.txt).
| Field | Type | Description |
|---|---|---|
| `client_name` | String | Human-readable name of the verifier. |
| `logo_uri` | String | URL pointing to the verifier's logo image. |
| `tos_uri` | String | URL to a human-readable Terms of Service document. |
| `policy_uri` | String | URL to a human-readable Privacy Policy document. |
| `client_uri` | String | URL to the verifier's homepage. |
| `vp_formats_supported` | Object | Credential and proof formats the verifier accepts (per OID4VP §11.1). If not set, the service automatically injects a default declaring `mso_mdoc` support with ES256, -9, and -50 issuer auth algorithms. |
| `jwks` | Object | JWK Set for wallet response encryption (per RFC 7517). For DC API flows with encrypted response (`encrypted_response: true`), this field is **ignored** — the service generates an ephemeral P-256 ECDH-ES key automatically. |
| `encrypted_response_enc_values_supported` | Array | JWE `enc` algorithms supported for encrypting the VP response. For DC API encrypted response flows, this is **hardcoded to `["A128GCM"]`** by the service and any configured value is overridden. |
### `urlPrefix`
_String_ — The base URL the service will use to construct the `request_uri` and `response_uri` endpoints
returned to wallets. Wallets fetch the authorization request from `{urlPrefix}/{sessionId}/request` and post
their VP response to `{urlPrefix}/{sessionId}/response`. Must be publicly reachable by the wallet.
**Error:**
`urlPrefix` is only used for OID4VP flows (cross-device and same-device). For DC API flows, this field must be
absent — passing it with a DC API request will cause session creation to fail with a validation error.
Example: `"http://localhost:7004/verification-session"`
### `urlHost`
_String_ — The base URL used to construct the QR code link or deep link handed to the wallet. The authorization
request URL is built by appending the authorization request parameters to this value.
- Use `"openid4vp://authorize"` for OID4VP flows where the wallet is invoked via a deep link or QR code scan.
- Use a full HTTPS URL for conformance testing with a browser-based authorization endpoint.
For DC API flows, `urlHost` is not used from config — it is derived from `expectedOrigins` (Annex D) or `origin`
(Annex C) provided in the request body.
### `key` _(optional)_
_JWK Object_ — A JSON Web Key used to sign the authorization request JWT. The key must be provided in JWK
format wrapped in a `{"type":"jwk","jwk":{...}}` object.
**Note:**
The `key` field does not control whether requests are signed. Signing is enabled per-request by setting
`signed_request: true` in the verification session setup. When `signed_request: true`, a signing key is
**required** — either configured here or supplied per-request. If neither is present, session creation will fail.
### `x5c` _(optional)_
_Array of Strings_ — An ordered X.509 certificate chain in DER-encoded, Base64-encoded format. The first
certificate must correspond to the `key`. Required when using `x509_san_dns:` or `x509_hash:` as the `clientId`
format, as wallets will use the certificate chain to verify the verifier's identity.
For signed Annex C (ISO 18013-7) requests, `x5c` is explicitly required — session creation will fail without it.
## Full Example
```editorconfig[verifier-service.conf]
clientId: "verifier2"
# clientId: "x509_san_dns:verifier.example.com"
# clientId: "x509_hash:"
clientMetadata: {
client_name: "My Verifier"
"client_name#fr-FR": "Mon Vérificateur"
logo_uri: "https://example.com/logo.png"
tos_uri: "https://example.com/terms"
"tos_uri#fr-FR": "https://example.com/terms-fr"
policy_uri: "https://example.com/privacy"
"policy_uri#fr-FR": "https://example.com/privacy-fr"
client_uri: "https://example.com"
}
# Used for OID4VP flows only. Not applicable to DC API flows.
urlPrefix: "http://localhost:7004/verification-session"
urlHost: "openid4vp://authorize"
# Required when signed_request: true is set in the verification session setup.
# key: {"type":"jwk","jwk":{"kty":"EC","crv":"P-256",...}}
# Required when using x509_san_dns or x509_hash clientId formats.
# x5c: ["MIIBVz..."]
```
--------------------------------------------------------------------------------
PAGE: Dev Mode
URL: https://docs.walt.id/community-stack/verifier2/configurations/config-files/dev-mode
RAW: https://docs.walt.id/community-stack/verifier2/configurations/config-files/dev-mode.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Dev Mode
`dev-mode.conf` contains settings that are only relevant in local development and testing environments.
This file is only loaded when the `dev-mode` feature is explicitly enabled in
[`_features.conf`](https://docs.walt.id/community-stack/verifier2/configurations/config-files/features.md).
## Fields
### `enableDidWebResolverHttps`
_Boolean — default: `true`_
Controls whether the DID Web resolver enforces HTTPS. When `true`, only HTTPS DID Web endpoints are accepted.
Set to `false` in local development environments where HTTPS is not available.
This field has no effect unless the `dev-mode` feature is enabled in `_features.conf`.
**Note:**
The code default is `true` (HTTPS enforced). The example `dev-mode.conf` shipped in the repository sets this to
`false`, which is the typical local development value. If you enable dev-mode but do not provide a `dev-mode.conf`,
HTTPS will be enforced.
## Example
```editorconfig[dev-mode.conf]
# Allow HTTP for local DID Web resolution (typical dev setup)
enableDidWebResolverHttps = false
```
## Enabling Dev Mode
Add `dev-mode` to the `enabledFeatures` list in `_features.conf`. The `dev-mode.conf` file is only loaded
when this feature is active — it has no effect otherwise.
```editorconfig[_features.conf]
enabledFeatures = [
dev-mode
]
```
--------------------------------------------------------------------------------
PAGE: Transaction Data Profiles
URL: https://docs.walt.id/community-stack/verifier2/configurations/config-files/transaction-data-profiles
RAW: https://docs.walt.id/community-stack/verifier2/configurations/config-files/transaction-data-profiles.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Transaction Data Profiles
`transaction-data-profiles.conf` defines the transaction data types the verifier and wallet recognize for binding presentations to specific actions (e.g., payment authorization, account access, SCA payment).
Transaction data types act as namespaces that:
- Enforce which types the wallet will accept in authorization requests
- Define the structure and fields for UI/discovery endpoints
- Are validated by the wallet's `TransactionDataTypeRegistry`
- Are used as the DeviceSigned namespace for `mso_mdoc` transaction-data binding
The `transaction-data-profiles` feature is enabled by default for Verifier2 and backs the runtime discovery endpoint:
`GET /transaction-data-profiles`
The same profile format is also used by the walt.id Wallet API (see [Wallet2 transaction-data profiles](https://docs.walt.id/community-stack/wallet2/configurations/config-files/transaction-data-profiles.md))
**Info:**
**mDoc issuance grant:** For `mso_mdoc` credentials, registering a type here is not enough for the holder to device-sign it. The credential's MSO must authorize that type via `KeyAuthorizations`, which Issuer2 sets from the profile field [`authorizedTransactionDataTypes`](https://docs.walt.id/community-stack/issuer2/configurations/config-files/issuer-profiles.md#authorized-transaction-data-types). SD-JWT VC presentations bind transaction data in the KB-JWT and do not use that field.
## Fields
### `transactionDataProfiles`
_Array of Objects_ — List of recognized transaction data types. Each profile defines:
| Field | Type | Description |
|---|---|---|
| `type` | String | **Required.** The transaction data type identifier. Used as the `type` field in transaction data entries and as the namespace for mdoc DeviceSigned binding. Example: `"org.waltid.transaction-data.payment-authorization"` |
| `displayName` | String | Human-readable name for this type. Used in UIs and discovery endpoints. Defaults to `type` if not specified. |
| `fields` | Array | List of expected top-level field names for this type. Used for documentation and UI generation. Nested payloads (e.g. SCA) can declare a single container field such as `["payload"]`. |
## Built-in Profiles
The default configuration includes:
```editorconfig[transaction-data-profiles.conf]
transactionDataProfiles = [
{
type = "org.waltid.transaction-data.payment-authorization"
displayName = "Payment Authorization"
# Android Credential Manager matcher reads merchant_name and amount for generic payment entries.
fields = ["merchant_name", "amount", "currency"]
},
{
type = "org.waltid.transaction-data.account-access"
displayName = "Account Access"
fields = ["account_identifier", "access_scope"]
},
{
# EUDI TS-12 SCA payment type. Payload nests, so the matcher reads
# payload.payee.name, payload.currency and payload.amount rather than flat fields.
type = "urn:eudi:sca:payment:1"
displayName = "SCA Payment"
fields = ["payload"]
},
{
# Interop identifier used by some OpenID4VP payment-card verifiers. Prefer a
# collision-resistant type for new deployments.
type = "payment_card"
displayName = "Payment Card"
fields = ["merchant_name", "amount"]
}
]
```
| Type | Shape | Notes |
|------|--------|--------|
| `org.waltid.transaction-data.payment-authorization` | Flat fields | walt.id payment demo type |
| `org.waltid.transaction-data.account-access` | Flat fields | Account access demo type |
| `urn:eudi:sca:payment:1` | Nested `payload` | EUDI TS-12 SCA payment; `amount` is a JSON number inside `payload` |
| `payment_card` | Flat `merchant_name`, `amount` | Interop identifier used by some OpenID4VP payment-card verifiers. Prefer a collision-resistant type for new deployments. `amount` may be a combined string such as `"EUR 200.00"`. |
## Discovery
The `transaction-data-profiles` feature is enabled by default and loads `transaction-data-profiles.conf`. Community Verifier2 exposes that HOCON list at:
`GET /transaction-data-profiles`
Community Verifier2 does not expose `GET /transaction-data-profiles/{type}` or create, replace, or delete. Session create uses **structure-only** validation: it checks `transaction_data` shape and DCQL binding, and does not apply a type allowlist. The [Wallet2 type registry](https://docs.walt.id/community-stack/wallet2/configurations/config-files/transaction-data-profiles.md) still rejects unknown `transaction_data.type` values when that feature is enabled.
Runtime overlay mutations are not exposed on Community Verifier2. Durable types live in `transaction-data-profiles.conf`. Per-service create, replace, delete, and session-create type enforcement live on [Enterprise Wallet2 and Verifier2](https://docs.walt.id/enterprise-stack/setup/configurations/config-files/transaction-data-profiles.md).
Wallet API v1 exposes `GET /transaction-data-profiles` only.
## Adding Custom Types
To add a custom transaction data type, add a new entry to the `transactionDataProfiles` array in both config files and restart:
- Verifier: `waltid-services/waltid-verifier-api2/config/transaction-data-profiles.conf`
- Wallet: `waltid-services/waltid-wallet-api2/config/transaction-data-profiles.conf`
For mDoc holders that must device-sign the type, also list it on the Issuer2 profile as [`authorizedTransactionDataTypes`](https://docs.walt.id/community-stack/issuer2/configurations/config-files/issuer-profiles.md#authorized-transaction-data-types)
**Example:** Adding a subscription authorization type:
```editorconfig[transaction-data-profiles.conf]
transactionDataProfiles = [
{
type = "org.waltid.transaction-data.payment-authorization"
displayName = "Payment Authorization"
fields = ["merchant_name", "amount", "currency"]
},
{
type = "org.waltid.transaction-data.account-access"
displayName = "Account Access"
fields = ["account_identifier", "access_scope"]
},
{
type = "urn:eudi:sca:payment:1"
displayName = "SCA Payment"
fields = ["payload"]
},
{
type = "org.example.subscription-authorization"
displayName = "Subscription Authorization"
fields = ["service_name", "billing_cycle", "price"]
}
]
```
**Info:**
**Important:** Transaction data types must be configured on **both** the verifier and wallet. Mismatched configurations will cause the wallet to reject authorization requests with unknown types.
## Usage in Verification Requests
Once configured, use the `type` in your verification session transaction data.
Flat walt.id payment type:
```json
{
"flow_type": "cross_device",
"core_flow": {
"dcql_query": { "...": "..." }
},
"openid": {
"transactionData": [
{
"type": "org.waltid.transaction-data.payment-authorization",
"credential_ids": ["pid"],
"require_cryptographic_holder_binding": true,
"transaction_data_hashes_alg": ["sha-256"],
"merchant_name": "ACME Corp",
"amount": "42.00",
"currency": "EUR"
}
]
}
}
```
Nested SCA payment type:
```json
{
"openid": {
"transactionData": [
{
"type": "urn:eudi:sca:payment:1",
"credential_ids": ["sca_payment_card"],
"require_cryptographic_holder_binding": true,
"transaction_data_hashes_alg": ["sha-256"],
"payload": {
"transaction_id": "8D8AC610-566D-4EF0-9C22-186B2A5ED793",
"payee": {
"name": "Super Store",
"id": "merchant-001"
},
"currency": "EUR",
"amount": 11.56
}
}
]
}
}
```
See [Transaction Data Authorization](https://docs.walt.id/community-stack/verifier2/credential-verification/sd-jwt-vc-oid4vp.md#transaction-data-authorization) for complete usage details.
--------------------------------------------------------------------------------
PAGE: Full API Reference
URL: https://docs.walt.id/community-stack/verifier2/full-api-reference
RAW: https://docs.walt.id/community-stack/verifier2/full-api-reference.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Full API Reference
Find it [here](https://verifier2.demo.walt.id/swagger/index.html)
--------------------------------------------------------------------------------
PAGE: Migrating from Verifier (v1)
URL: https://docs.walt.id/community-stack/verifier2/migration-from-verifier1
RAW: https://docs.walt.id/community-stack/verifier2/migration-from-verifier1.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Migrating from Verifier (v1) to Verifier2
This guide helps you migrate from the original Verifier service (which supports [**OpenID for Verifiable Presentations (OID4VP)**](https://docs.walt.id/concepts/data-exchange-protocols/openid4vp.md) Draft 14 and Draft 20) to Verifier2 (which supports OID4VP 1.0 final specification).
**Error:**
The original Verifier service is deprecated. We recommend migrating to Verifier2 for all new projects and planning migration for existing production deployments.
---
## Key Differences at a Glance
| Aspect | Verifier (v1) | Verifier2 |
|--------|--------------|-----------|
| **OID4VP Version** | Draft 14, Draft 20 | 1.0 (final specification) |
| **Port (Docker Compose)** | 7003 | 7004 |
| **Port (Standalone)** | 7003 | 7004 |
| **Request Structure** | Single flat request body | Structured: `flow_type` + `core_flow` + flow-specific attributes |
| **Credential Query** | `request_credentials` | `dcql_query` (DCQL standard) |
| **Policies** | Separate `vp_policies` and `vc_policies` arrays | Nested under `policies` object |
| **URL Configuration** | Headers (`authorizeBaseUrl`, `successRedirectUri`, `errorRedirectUri`) | Request body fields (`url_config`, `redirects`) |
| **Flow Selection** | Implicit (via `preset` or config) | Explicit via `flow_type` field |
| **Session Endpoint** | `POST /openid4vc/verify` | `POST /verification-session/create` |
---
## Request Structure Changes
### Verifier (v1) Request Example
```bash
curl -X 'POST' \
'http://localhost:7003/openid4vc/verify' \
-H 'authorizeBaseUrl: openid4vp://authorize' \
-H 'successRedirectUri: https://example.com/success' \
-H 'errorRedirectUri: https://example.com/error' \
-H 'Content-Type: application/json' \
-d '{
"request_credentials": [
{
"format": "jwt_vc_json",
"type": ["VerifiableCredential", "OpenBadgeCredential"]
}
],
"vp_policies": [],
"vc_policies": [
"signature",
"expired"
]
}'
```
### Verifier2 Equivalent
```bash
curl -X 'POST' \
'http://localhost:7004/verification-session/create' \
-H 'Content-Type: application/json' \
-d '{
"flow_type": "cross_device",
"url_config": {
"url_host": "openid4vp://authorize",
"url_prefix": "http://localhost:7004/verification-session"
},
"redirects": {
"successRedirectUri": "https://example.com/success",
"errorRedirectUri": "https://example.com/error"
},
"core_flow": {
"dcql_query": {
"credentials": [
{
"id": "my_credential",
"format": "jwt_vc_json",
"meta": {
"type_values": [
["VerifiableCredential", "OpenBadgeCredential"]
]
}
}
]
},
"policies": {
"vc_policies": [
{ "policy": "signature" },
{ "policy": "expired" }
]
}
}
}'
```
---
## Breaking Changes
### 1. Request Body Structure
**v1:** Single flat JSON object
**v2:** Structured with `flow_type`, `core_flow`, and flow-specific sections
**Migration:** Decompose your v1 request into:
- `flow_type` — Choose `cross_device`, `same_device`, or `dc_api`
- `core_flow` — Move credential queries and policies here
- `url_config` — Move URL settings here (no longer in headers)
- `redirects` — Move redirect URLs here (no longer in headers)
---
### 2. Credential Query Format
**v1:** `request_credentials` with simple `type` array
```json
{
"request_credentials": [
{
"format": "jwt_vc_json",
"type": ["VerifiableCredential", "OpenBadgeCredential"]
}
]
}
```
**v2:** `dcql_query` using the DCQL standard
```json
{
"dcql_query": {
"credentials": [
{
"id": "my_credential",
"format": "jwt_vc_json",
"meta": {
"type_values": [
["VerifiableCredential", "OpenBadgeCredential"]
]
}
}
]
}
}
```
**Key Changes:**
- Each credential query requires a unique `id`
- `type` becomes `meta.type_values` (array of arrays to support alternatives)
- DCQL adds support for claim-level queries via `claims` array
---
### 3. Policy Configuration
**v1:** Separate top-level arrays
```json
{
"vp_policies": [],
"vc_policies": ["signature", "expired"]
}
```
**v2:** Nested under `policies` object with explicit policy objects
```json
{
"policies": {
"vc_policies": [
{ "policy": "signature" },
{ "policy": "expired" }
],
"vp_policies": []
}
}
```
**Key Changes:**
- Each policy is now an object with a `policy` field (allows for policy arguments in future)
- Nested under `policies` object instead of top-level
---
### 4. URL Configuration
**v1:** Passed via HTTP headers
```bash
-H 'authorizeBaseUrl: openid4vp://authorize' \
-H 'successRedirectUri: https://example.com/success' \
-H 'errorRedirectUri: https://example.com/error'
```
**v2:** Passed in request body
```json
{
"url_config": {
"url_host": "openid4vp://authorize",
"url_prefix": "http://localhost:7004/verification-session"
},
"redirects": {
"successRedirectUri": "https://example.com/success",
"errorRedirectUri": "https://example.com/error"
}
}
```
**Key Changes:**
- `authorizeBaseUrl` → `url_config.url_host`
- `successRedirectUri` and `errorRedirectUri` → nested under `redirects` object
- New `url_config.url_prefix` field (was implicit in v1)
---
### 5. Endpoint Changes
| Operation | Verifier (v1) | Verifier2 |
|-----------|--------------|-----------|
| Create verification session | `POST /openid4vc/verify` | `POST /verification-session/create` |
| Get session result | `GET /openid4vc/session/{id}` | `GET /verification-session/{id}/info` |
---
### 6. Response Format Changes
**v1 Session Response:**
```json
"openid4vp://authorize?..."
```
**v2 Session Response:**
```json
{
"sessionId": "abc123",
"bootstrapAuthorizationRequestUrl": "openid4vp://authorize?...",
"fullAuthorizationRequestUrl": "openid4vp://authorize?..."
}
```
**Key Changes:**
- `url` field split into `bootstrapAuthorizationRequestUrl` (short) and `fullAuthorizationRequestUrl` (with all params)
---
## Configuration File Changes
### Verifier (v1) Config
```editorconfig[verifier.conf]
baseUrl: "http://localhost:7003"
authorizeBaseUrl: "openid4vp://authorize"
```
### Verifier2 Config
```editorconfig[verifier-service.conf]
clientId: "verifier2"
clientMetadata: {
client_name: "My Verifier"
logo_uri: "https://example.com/logo.png"
}
urlPrefix: "http://localhost:7004/verification-session"
urlHost: "openid4vp://authorize"
```
See [Verifier Service Configuration](https://docs.walt.id/community-stack/verifier2/configurations/config-files/verifier-service.md) for complete configuration documentation.
---
--------------------------------------------------------------------------------
PAGE: Overview
URL: https://docs.walt.id/community-stack/verifier2/apps/portal2/overview
RAW: https://docs.walt.id/community-stack/verifier2/apps/portal2/overview.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Overview
**Portal2** is a white-label web app for credential issuance and verification using the walt.id
**Issuer2** and **Verifier2** APIs.
## Features
- **Issue credentials** — Issue a credential from a custom JSON payload or a pre-configured [Issuer2 profile](https://docs.walt.id/community-stack/issuer2/credential-profiles/overview.md) to a wallet via [**OpenID for Verifiable Credential Issuance (OID4VCI)**](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md) v1.
- **Verify credentials** — request a credential presentation from a wallet via [**OpenID for Verifiable Presentations (OID4VP)**](https://docs.walt.id/concepts/data-exchange-protocols/openid4vp.md) v1.
- **Simple and Advanced modes** — a guided form for common credential types, or a raw JSON editor for full control over the request payload. Simple mode is the default.
- **Load examples** — payloads can be auto-populated from the Issuer2 and Verifier2 OpenAPI/Swagger specs.
- **Real-time event log** — issuance and verification steps (e.g. `OFFER_RECEIVED`, `CREDENTIAL_ISSUED`) stream in via Server-Sent Events as the wallet interacts with the offer or request.
## Try It Out
- **Live demo:** [portal2.demo.walt.id](https://portal2.demo.walt.id)
- **Local:** included in the [Quick Start](https://docs.walt.id/community-stack/home.md#quick-start) Docker Compose — once running, open [localhost:7105](http://localhost:7105).
## Setup
1. Clone the identity repo
```bash
git clone https://github.com/walt-id/waltid-identity.git && cd waltid-identity
```
2. Running the project
**Option: Local**
1. Change directory
```bash
cd waltid-applications/waltid-web-portal2/
```
2. Configure the dependency services
Create a `.env` file:
```text
NUXT_PUBLIC_ISSUER_BASE=http://localhost:7005
NUXT_PUBLIC_VERIFIER_BASE=http://localhost:7004
NUXT_PUBLIC_WALLET_URL=http://localhost:7101
```
**Note:**
`NUXT_PUBLIC_WALLET_URL` points at the demo [Web Wallet](https://docs.walt.id/community-stack/wallet/apps/overview.md) — there is no Wallet API v2 app yet. Three optional variables (`NUXT_PUBLIC_VERIFIER_KEY_JWK`, `NUXT_PUBLIC_VERIFIER_X5C`, `NUXT_PUBLIC_VERIFIER_CLIENT_ID`) pre-populate the advanced verification security section; leave them empty unless you need them.
3. Run the development server
```bash
npm install
npm run dev
```
The app is available at [localhost:3000](http://localhost:3000).
**Option: Docker**
1. Build & run the image
```bash
docker build -t waltid/portal2 -f waltid-applications/waltid-web-portal2/Dockerfile .
docker run -p 3000:3000 \
-e NUXT_PUBLIC_ISSUER_BASE="http://localhost:7005" \
-e NUXT_PUBLIC_VERIFIER_BASE="http://localhost:7004" \
-e NUXT_PUBLIC_WALLET_URL="http://localhost:7101" \
-i -t waltid/portal2
```
2. Access the portal under [localhost:3000](http://localhost:3000/)
--------------------------------------------------------------------------------
PAGE: Getting Started
URL: https://docs.walt.id/community-stack/issuer2/getting-started
RAW: https://docs.walt.id/community-stack/issuer2/getting-started.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Getting Started
**Issuer2** is an open-source HTTP API service for creating, signing, and distributing verifiable digital credentials. It implements the [OID4VCI v1.0](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md) specification and introduces a profile-based architecture for streamlined credential issuance. Supported credential formats include [W3C VC](https://docs.walt.id/concepts/digital-credentials/verifiable-credentials-w3c.md), [SD-JWT VC (IETF)](https://docs.walt.id/concepts/digital-credentials/sd-jwt-vc.md), and [ISO 18013-5 mDL](https://docs.walt.id/concepts/digital-credentials/mdoc-mdl-iso.md).
**Note:**
The original issuer service, which supports Draft 11/13 of the OID4VCI specification, is deprecated. It remains available for existing projects. For new projects, we recommend using **Issuer2** with its profile-based architecture and OID4VCI v1.0 support.
**Note:**
**Coming from the original Issuer (v1)?** The biggest change is that each credential type must be declared ahead of time as a **profile** in a config file — including a default issuer key and credential data — and the service must be restarted to pick up new or changed profiles. You no longer need to supply a signing key or credential data with every issuance request, though you can still override either (and more) per-offer at request time via [`runtimeOverrides`](https://docs.walt.id/community-stack/issuer2/credential-offers/create-offer.md#runtime-overrides). See [Core Concepts](#core-concepts) below.
## Supported Standards
| | |
|:-------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **Credential Formats:** | [SD-JWT VC (IETF)](https://docs.walt.id/concepts/digital-credentials/sd-jwt-vc.md), [W3C VC (v1.1+, v2.0)](https://docs.walt.id/concepts/digital-credentials/verifiable-credentials-w3c.md), [ISO 18013-5 mDL](https://docs.walt.id/concepts/digital-credentials/mdoc-mdl-iso.md) |
| **Credential Exchange:** | [OID4VCI](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md) (v1.0) |
| **Signing Algorithms:** | ed25519, secp256k1, secp256r1, RSA |
## Setup
If you are new to the stack, the **Docker Compose quick start** is the fastest way to get running — it spins up the Issuer, Verifier, and Wallet together in one command, which mirrors a real issuance flow. Once running, the API is available at `http://localhost:7005`.
**Note:**
Issuer2 listens on port `7005` by default, whether run standalone or via Docker Compose. The original Issuer (v1) runs on port `7002`, avoiding a conflict when both run together.
- [Docker Compose quick start](https://docs.walt.id/community-stack/home.md#quick-start) - Spin up the issuer API locally together with the other services. _(Recommended for first-time users)_
- [Run standalone with Docker](https://docs.walt.id/community-stack/issuer2/setup.md) - Pull and run the issuer container in isolation using the [walt.id identity repo](https://github.com/walt-id/waltid-identity.git). See the [configuration reference](https://docs.walt.id/community-stack/issuer2/configurations/feature-manager.md) for available config options.
- [Full API reference](https://docs.walt.id/community-stack/issuer2/full-api-reference.md) - Explore all available endpoints via Swagger.
## Core Concepts
### Profile-Based Issuance
Issuer2 introduces a **profile-based architecture** that simplifies credential issuance:
- **Credential Profiles** – Define reusable configurations for each credential type, including issuer keys, credential data templates, selective disclosure settings, and notification webhooks.
- **Credential Offers** – Create offers from profiles with optional runtime overrides, supporting both pre-authorized and authorization code flows.
### Credential Exchange
The Issuer2 API supports credential exchange protocols based on:
- **OID4VCI v1.0:** Flows such as Pre-Authorized Code Flow (with or without PIN/Transaction Code) and Authorization Code Flow (with external authorization servers like Keycloak).
- **mDoc Issuance:** Remote issuance of Mobile Driver's Licenses (mDL) and other ISO 18013-5 credentials via OID4VCI (ISO 18013-7).
### Credential Data Collection
Flexible data collection options allow populating credentials before or after an offer has been created:
- **Before Credential Offer Creation** – Provide all subject data upfront when creating the profile or offer.
- **After Credential Offer Creation & Before Credential Signing** – Enrich credentials dynamically using [data functions](https://docs.walt.id/community-stack/issuer2/data-functions.md) such as webhooks or timestamps.
- **During User Authentication** – When using the authorization code flow, the subject can authenticate against an external IdP and the retrieved claims are mapped to credential fields via `idTokenClaimsMapping`.
## Features
- [Credential Profiles](https://docs.walt.id/community-stack/issuer2/credential-profiles/overview.md) - Create and manage reusable credential configurations.
- [Credential Offers](https://docs.walt.id/community-stack/issuer2/credential-offers/overview.md) - Create credential offers from profiles with runtime overrides.
- [Data Functions](https://docs.walt.id/community-stack/issuer2/data-functions.md) - Populate credentials with dynamic data at claim time.
- [Notifications](https://docs.walt.id/community-stack/issuer2/notifications.md) - Configure webhook notifications to stay informed about on the issuance.
## Start Issuing
Choose the credential type that matches your use case:
- [SD-JWT VC Issuance](https://docs.walt.id/community-stack/issuer2/credential-issuance/sd-jwt-vc.md) - Issue IETF SD-JWT Credentials.
- [W3C VC Issuance](https://docs.walt.id/community-stack/issuer2/credential-issuance/w3c-vc.md) - Issue W3C Verifiable Credentials with JWT signatures.
- [mDL Issuance](https://docs.walt.id/community-stack/issuer2/credential-issuance/mdl.md) - Issue ISO 18013-5 Mobile Driver's Licenses.
--------------------------------------------------------------------------------
PAGE: Setup
URL: https://docs.walt.id/community-stack/issuer2/setup
RAW: https://docs.walt.id/community-stack/issuer2/setup.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Setup
## Installation & Running the Project
Make sure you have Docker installed on your machine.
### Option 1: Using Docker Pull
**1. Clone the Identity Repo**
Clone the repository to your local machine. The repo contains the default configuration files that will be mounted into the container in the next step.
```bash
git clone https://github.com/walt-id/waltid-identity.git && cd waltid-identity
```
**2. Pull the Docker Image**
Pull the pre-built Docker image from Docker Hub:
```bash
docker pull waltid/issuer-api2
```
**3. Run the Docker Image**
Run the container, mounting the config directory from the cloned repository. This gives the service its default configuration — see the note below for details.
```bash
docker run -p 7005:7005 \
-itv $(pwd)/waltid-services/waltid-issuer-api2/config:/waltid-issuer-api2/config \
-t waltid/issuer-api2
```
The `-v` flag mounts `waltid-services/waltid-issuer-api2/config` from the cloned repository into the container. This directory contains the service configuration files (e.g. server port, credential profiles, metadata). You can edit these files before starting the container to customise the service behaviour.
**Note:**
See the [configuration reference](https://docs.walt.id/community-stack/issuer2/configurations/feature-manager.md) for a full description of all available config files and their fields.
Once the container starts, the API is available at `http://localhost:7005`. You can confirm it is running by opening `http://localhost:7005/swagger` in your browser.
### Option 2: Building the Image Yourself
**Note:**
This option requires **Java (JDK 17+)**, **Gradle**, and **Docker** to be installed on your machine.
**1. Clone the Identity Repo**
Clone the repository to your local machine:
```bash
git clone https://github.com/walt-id/waltid-identity.git && cd waltid-identity
```
**2. Build the Docker Image**
If you prefer to build the Docker image yourself, use the Gradle Ktor/Jib tasks:
```bash
# Development (local Docker daemon, single-arch)
./gradlew :waltid-services:waltid-issuer-api2:publishImageToLocalRegistry
# image: waltid/issuer-api2:
```
```bash
# Production (multi-arch push to your registry)
export DOCKER_USERNAME=
export DOCKER_PASSWORD=
./gradlew :waltid-services:waltid-issuer-api2:publishImage
# image: docker.io//issuer-api2:
```
Note: multi-arch images require a registry push. Local tar output is single-arch only.
**3. Run the Docker Image**
After building the image, run it using (adjust the image name if you pushed to your registry):
```bash
docker run -p 7005:7005 \
-itv $(pwd)/waltid-services/waltid-issuer-api2/config:/waltid-issuer-api2/config \
-t waltid/issuer-api2
```
The `-v` flag mounts `waltid-services/waltid-issuer-api2/config` from the cloned repository into the container. This directory contains the service configuration files (e.g. server port, credential profiles, metadata). You can edit these files before starting the container to customise the service behaviour.
**Note:**
See the [configuration reference](https://docs.walt.id/community-stack/issuer2/configurations/feature-manager.md) for a full description of all available config files and their fields.
Once the container starts, the API is available at `http://localhost:7005`. You can confirm it is running by opening `http://localhost:7005/swagger` in your browser.
## Start Issuing Credentials
Now that the Issuer2 API is running, you can start issuing credentials:
1. [Issue a Credential](https://docs.walt.id/community-stack/issuer2/credential-issuance/sd-jwt-vc.md) - Issue your first credential from a ready-made profile, no configuration needed.
2. [Configure Credential Profiles](https://docs.walt.id/community-stack/issuer2/credential-profiles/overview.md) - Define reusable configurations for your own credential types.
3. [Create Credential Offers](https://docs.walt.id/community-stack/issuer2/credential-offers/overview.md) - Generate [OID4VCI](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md) credential offer URLs.
4. [Full API Reference](https://docs.walt.id/community-stack/issuer2/full-api-reference.md) - Explore all available endpoints via Swagger.
--------------------------------------------------------------------------------
PAGE: Issuing SD-JWT VC Credentials via OID4VCI
URL: https://docs.walt.id/community-stack/issuer2/credential-issuance/sd-jwt-vc
RAW: https://docs.walt.id/community-stack/issuer2/credential-issuance/sd-jwt-vc.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Issuing SD-JWT VC Credentials via OID4VCI
Video: https://youtu.be/7Pekfj2ipt8
This guide shows you how to issue an [IETF SD-JWT VC](https://docs.walt.id/concepts/digital-credentials/sd-jwt-vc.md) credential using the walt.id Issuer2 API. You'll issue a credential from a ready-made [profile](https://docs.walt.id/community-stack/issuer2/credential-profiles/overview.md) in seconds, then see how to define your own.
[**SD-JWT VC**](https://docs.walt.id/concepts/digital-credentials/sd-jwt-vc.md): An IETF credential format supporting selective disclosure, letting holders reveal only specific claims of a credential rather than the whole thing.
[**OID4VCI**](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md): The protocol used to deliver the credential to a wallet.
**Note:**
Issuer2 issues credentials from **profiles** — reusable configurations declared ahead of time in a config file, with sensible defaults for the issuer key and credential data. If you're used to the original Issuer (v1), where the key and credential data were posted directly to an issuance endpoint every time, see [Getting Started](https://docs.walt.id/community-stack/issuer2/getting-started.md#core-concepts) for what changed — you can still override a profile's defaults per-offer via [`runtimeOverrides`](https://docs.walt.id/community-stack/issuer2/credential-offers/create-offer.md#runtime-overrides).
## Prerequisites
Before you begin, ensure you have:
- **Issuer2 API running** — Follow the [Setup](https://docs.walt.id/community-stack/issuer2/setup.md) guide
- **A test wallet** — Use the [walt.id Wallet](https://wallet.demo.walt.id/) for testing, or any OID4VCI-compatible wallet
**Note:**
**Local Development:** When running locally via Docker, the API is available at `http://localhost:7005` by default, whether standalone or via Docker Compose. Use this as your base URL in the examples below.
---
## Quick Start: Issue Using the Default Profile
The waltid-identity repository ships with a ready-made profile called `identityCredentialSdJwt`, bound to the `identity_credential` credential type. You can issue from it immediately — no configuration file editing required.
### Step 1: Confirm the Profile Is Loaded
**Option: CURL**
**Endpoint:** `GET /issuer2/profiles/{profileId}` | [API Reference](https://issuer2.demo.walt.id/swagger/index.html)
##### Example Request
```bash
curl -X 'GET' \
'http://localhost:7005/issuer2/profiles/identityCredentialSdJwt' \
-H 'accept: application/json'
```
---
##### Example Response
```json
{
"profileId": "identityCredentialSdJwt",
"name": "Identity Credential",
"credentialConfigurationId": "identity_credential",
"issuerKey": {
"type": "jwk",
"jwk": {
"d": "KJ4k3Vcl5Sj9Mfq4rrNXBm2MoPoY3_Ak_PIR_EgsFhQ",
"crv": "P-256",
"x": "G0RINBiF-oQUD3d5DGnegQuXenI29JDaMGoMvioKRBM",
"y": "ed3eFGs2pEtrp7vAZ7BLcbrUtpKkYWAT2JPUQK4lN4E",
"kty": "EC"
}
},
"issuerDid": "did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IkcwUklOQmlGLW9RVUQzZDVER25lZ1F1WGVuSTI5SkRhTUdvTXZpb0tSQk0iLCJ5IjoiZWQzZUZHczJwRXRycDd2QVo3QkxjYnJVdHBLa1lXQVQySlBVUUs0bE40RSJ9",
"credentialData": {
"address": {
"region": "Anystate",
"street_address": "123 Main St",
"country": "US",
"locality": "Anytown"
},
"is_over_18": true,
"phone_number": "+1-202-555-0101",
"given_name": "John",
"family_name": "Doe",
"is_over_21": true,
"is_over_65": true,
"email": "johndoe@example.com",
"birthdate": "1940-01-01"
},
"mapping": {
"exp": "",
"nbf": "",
"id": "",
"iat": ""
},
"selectiveDisclosure": {
"fields": {
"birthdate": {
"sd": true
},
"family_name": {
"sd": false
}
},
"decoyMode": "NONE",
"decoys": 0
},
"idTokenClaimsMapping": {
"$.given_name": "$.given_name",
"$.family_name": "$.family_name"
}
}
```
The profile already has selective disclosure and issuer key/DID configured — you don't need to understand every field yet to issue your first credential. We'll come back to them in [Defining Your Own Profile](#defining-your-own-profile) below.
### Step 2: Create a Credential Offer
**Option: CURL**
**Endpoint:** `POST /issuer2/credential-offers` | [API Reference](https://issuer2.demo.walt.id/swagger/index.html)
##### Example Request
```bash
curl -X 'POST' \
'http://localhost:7005/issuer2/credential-offers' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"profileId": "identityCredentialSdJwt",
"authMethod": "PRE_AUTHORIZED"
}'
```
**Body Parameters**
- **profileId**: _String_ (required) - The profile to issue from. Here, the shipped default `identityCredentialSdJwt`.
- **authMethod**: _String_ (required) - The OID4VCI flow used to claim the offer. `PRE_AUTHORIZED` requires no user login and is the simplest way to get started. See [Using the Authorization Code Flow Instead](#using-the-authorization-code-flow-instead) for the alternative.
---
##### Example Response
```json
{
"offerId": "48f1dc54-134c-4e10-b889-17f3c0a595bf",
"profileId": "identityCredentialSdJwt",
"authMethod": "PRE_AUTHORIZED",
"expiresAt": 1782898162344,
"credentialOffer": "openid-credential-offer://?credential_offer_uri=http%3A%2F%2Flocalhost%3A7005%2Fopenid4vci%2Fcredential-offer%3Fid%3D48f1dc54-134c-4e10-b889-17f3c0a595bf"
}
```
**Response Fields**
- **offerId**: _String_ - Unique identifier for this offer.
- **expiresAt**: _Long_ - Unix timestamp (ms) when the offer expires (default: 300 seconds after creation).
- **credentialOffer**: _String_ - The OID4VCI credential offer URL. Turn this into a QR code or deep link for the wallet.
---
## Claiming the Offer
1. **Generate a QR code** from the `credentialOffer` URL, or use it directly as a deep link
2. **Open the [walt.id Wallet](https://wallet.demo.walt.id/)** and log in
3. **Click "Request Credential"** and paste the offer URL into the field below the camera
4. **The wallet resolves the offer**, requests the credential, and stores it
🎉 You've issued an SD-JWT VC credential using OID4VCI.
To learn how to accept this credential offer programmatically instead, see [How to Accept SD-JWT VC Credentials via OID4VCI](https://docs.walt.id/community-stack/wallet/credential-exchange/guides/accept-sd-jwt-vc-oid4vci.md).
---
## Defining Your Own Profile
To issue your own SD-JWT VC credential type instead of the shipped default, two config files need an entry, and the service needs a restart to pick them up.
### Step 1: Declare the Credential Type
Add an entry to `credential-issuer-metadata.conf`:
```hocon
credentialConfigurations = {
"my_identity_credential" = {
format = "dc+sd-jwt"
scope = "my_identity_credential"
vct = "vctBaseUrl"
cryptographic_binding_methods_supported = ["jwk", "did:key", "did:web"]
credential_signing_alg_values_supported = ["ES256"]
proof_types_supported = {
jwt = {
proof_signing_alg_values_supported = ["ES256"]
}
}
}
}
```
See [Credential Issuer Metadata](https://docs.walt.id/community-stack/issuer2/configurations/config-files/credential-issuer-metadata.md) for the full field reference.
### Step 2: Define a Profile
Add a matching profile to `issuer2-profiles.conf`:
```hocon
profiles {
myIdentityCredential {
name = "My Identity Credential"
credentialConfigurationId = "my_identity_credential"
issuerKey = ${defaultIssuerKey}
issuerDid = ${defaultIssuerDid}
credentialData = {
given_name = "Jane"
family_name = "Smith"
}
mapping = {
iat = ""
nbf = ""
}
selectiveDisclosure = {
fields = {
family_name = { sd = true }
}
}
}
}
```
See [Issuer Profiles Configuration](https://docs.walt.id/community-stack/issuer2/configurations/config-files/issuer-profiles.md) for every available profile field, including notifications and credential status.
### Step 3: Restart and Verify
**Error:**
Profiles are loaded once, at startup. **Restart the Issuer2 container** after editing either config file, then confirm the new profile is loaded.
```bash
curl -X 'GET' 'http://localhost:7005/issuer2/profiles/myIdentityCredential'
```
Then repeat the two steps from [Quick Start](#quick-start-issue-using-the-default-profile) above, substituting your own `profileId`.
---
## Using the Authorization Code Flow Instead
The example above used `PRE_AUTHORIZED` — no user login required. Issuer2 also supports the **Authorization Code Flow**, where the holder authenticates against an external IdP (e.g. Keycloak) before claiming the credential, and the resulting ID token claims can be mapped into the credential via `idTokenClaimsMapping`.
This requires registering an external OAuth 2.0 / OpenID Connect (OIDC) provider first. See [Authorization Code Flow](https://docs.walt.id/community-stack/issuer2/protocol-flows/authorization-code-flow.md) for the full sequence and setup.
## Selective Disclosure
The default profile already marks `birthdate` as selectively disclosable. To configure which claims a holder can choose to hide, see [Selective Disclosure](https://docs.walt.id/community-stack/issuer2/configurations/config-files/issuer-profiles.md#selective-disclosure) in the profile reference.
---
## Next Steps
**Info:**
The `vct` published for `identity_credential` is derived from your issuer's `baseUrl` (see [VCT handling](https://docs.walt.id/community-stack/issuer2/configurations/config-files/credential-issuer-metadata.md#vct-handling)). When verifying, use the `vct` from **your own** `/.well-known/openid-credential-issuer/openid4vci` response rather than assuming a fixed value.
- **Verify the credential you just issued** — Try [Verifying SD-JWT VC Credentials](https://docs.walt.id/community-stack/verifier2/credential-verification/sd-jwt-vc-oid4vp.md) with the walt.id Verifier2 API
- **Populate credentials dynamically** — See [Data Functions](https://docs.walt.id/community-stack/issuer2/data-functions.md) for webhooks, timestamps, and more
- **Track issuance events** — See [Notifications](https://docs.walt.id/community-stack/issuer2/notifications.md) for webhook and SSE options
- **Issue other credential types** — Try [W3C VC](https://docs.walt.id/community-stack/issuer2/credential-issuance/w3c-vc.md) or [mDL](https://docs.walt.id/community-stack/issuer2/credential-issuance/mdl.md)
--------------------------------------------------------------------------------
PAGE: Issuing W3C Verifiable Credentials via OID4VCI
URL: https://docs.walt.id/community-stack/issuer2/credential-issuance/w3c-vc
RAW: https://docs.walt.id/community-stack/issuer2/credential-issuance/w3c-vc.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Issuing W3C Verifiable Credentials via OID4VCI
Video: https://youtu.be/Lqz0yVDF6Qk
This guide shows you how to issue a [W3C Verifiable Credential](https://docs.walt.id/concepts/digital-credentials/verifiable-credentials-w3c.md) with a JWT signature using the walt.id Issuer2 API. You'll issue a credential from a ready-made profile in seconds, then see how to define your own.
[**W3C VC**](https://docs.walt.id/concepts/digital-credentials/verifiable-credentials-w3c.md): A digital equivalent of physical credentials such as a driver's license or university degree, defined by the W3C Verifiable Credentials Data Model.
[**OID4VCI**](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md): The protocol used to deliver the credential to a wallet.
**Note:**
Issuer2 issues credentials from **profiles** — reusable configurations declared ahead of time in a config file, with sensible defaults for the issuer key and credential data. If you're used to the original Issuer (v1), where the key and credential data were posted directly to an issuance endpoint every time, see [Getting Started](https://docs.walt.id/community-stack/issuer2/getting-started.md#core-concepts) for what changed — you can still override a profile's defaults per-offer via [`runtimeOverrides`](https://docs.walt.id/community-stack/issuer2/credential-offers/create-offer.md#runtime-overrides).
## Prerequisites
Before you begin, ensure you have:
- **Issuer2 API running** — Follow the [Setup](https://docs.walt.id/community-stack/issuer2/setup.md) guide
- **A test wallet** — Use the [walt.id Wallet](https://wallet.demo.walt.id/) for testing, or any OID4VCI-compatible wallet
**Note:**
**Local Development:** When running locally via Docker, the API is available at `http://localhost:7005` by default, whether standalone or via Docker Compose. Use this as your base URL in the examples below.
---
## Quick Start: Issue Using the Default Profile
The waltid-identity repository ships with a ready-made profile called `openBadgeCredential`, bound to the `OpenBadgeCredential_jwt_vc_json` credential type. You can issue from it immediately — no configuration file editing required.
### Step 1: Confirm the Profile Is Loaded
**Option: CURL**
**Endpoint:** `GET /issuer2/profiles/{profileId}` | [API Reference](https://issuer2.demo.walt.id/swagger/index.html)
##### Example Request
```bash
curl -X 'GET' \
'http://localhost:7005/issuer2/profiles/openBadgeCredential' \
-H 'accept: application/json'
```
---
##### Example Response
```json
{
"profileId": "openBadgeCredential",
"name": "OpenBadgeCredential",
"credentialConfigurationId": "OpenBadgeCredential_jwt_vc_json",
"issuerKey": {
"type": "jwk",
"jwk": {
"d": "KJ4k3Vcl5Sj9Mfq4rrNXBm2MoPoY3_Ak_PIR_EgsFhQ",
"crv": "P-256",
"x": "G0RINBiF-oQUD3d5DGnegQuXenI29JDaMGoMvioKRBM",
"y": "ed3eFGs2pEtrp7vAZ7BLcbrUtpKkYWAT2JPUQK4lN4E",
"kty": "EC"
}
},
"issuerDid": "did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IkcwUklOQmlGLW9RVUQzZDVER25lZ1F1WGVuSTI5SkRhTUdvTXZpb0tSQk0iLCJ5IjoiZWQzZUZHczJwRXRycDd2QVo3QkxjYnJVdHBLa1lXQVQySlBVUUs0bE40RSJ9",
"credentialData": {
"@context": [
"https://www.w3.org/ns/credentials/v2",
"https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json",
"https://purl.imsglobal.org/spec/ob/v3p0/extensions.json"
],
"id": "THIS WILL BE REPLACED WITH DYNAMIC DATA FUNCTION",
"type": [
"VerifiableCredential",
"OpenBadgeCredential"
],
"issuer": {
"type": ["Profile"],
"id": "THIS WILL BE REPLACED WITH DYNAMIC DATA FUNCTION",
"name": "Jobs for the Future (JFF)",
"url": "https://www.jff.org/",
"image": "https://w3c-ccg.github.io/vc-ed/plugfest-1-2022/images/JFF_LogoLockup.png"
},
"validFrom": "THIS WILL BE REPLACED WITH DYNAMIC DATA FUNCTION",
"name": "Example University Degree",
"credentialSubject": {
"id": "THIS WILL BE REPLACED WITH DYNAMIC DATA FUNCTION",
"type": ["AchievementSubject"],
"achievement": {
"id": "https://example.com/achievements/21st-century-skills/teamwork",
"type": ["Achievement"],
"criteria": {
"narrative": "Team members are nominated for this badge by their peers and recognized upon review by Example Corp management."
},
"description": "This badge recognizes the development of the capacity to collaborate within a group environment.",
"name": "Teamwork"
}
},
"credentialSchema": [
{
"id": "https://purl.imsglobal.org/spec/ob/v3p0/schema/json/ob_v3p0_achievementcredential_schema.json",
"type": "1EdTechJsonSchemaValidator2019"
}
]
},
"mapping": {
"id": "",
"issuer": {
"id": ""
},
"credentialSubject": {
"id": ""
},
"validFrom": ""
}
}
```
This is a minimal profile — no selective disclosure, no status list — a good first look at the plain shape of a profile before you customize one.
### Step 2: Create a Credential Offer
**Option: CURL**
**Endpoint:** `POST /issuer2/credential-offers` | [API Reference](https://issuer2.demo.walt.id/swagger/index.html)
##### Example Request
```bash
curl -X 'POST' \
'http://localhost:7005/issuer2/credential-offers' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"profileId": "openBadgeCredential",
"authMethod": "PRE_AUTHORIZED"
}'
```
**Body Parameters**
- **profileId**: _String_ (required) - The profile to issue from. Here, the shipped default `openBadgeCredential`.
- **authMethod**: _String_ (required) - The OID4VCI flow used to claim the offer. `PRE_AUTHORIZED` requires no user login and is the simplest way to get started. See [Using the Authorization Code Flow Instead](#using-the-authorization-code-flow-instead) for the alternative.
---
##### Example Response
```json
{
"offerId": "477ecf7e-e783-4326-85dd-a590e94d06e6",
"profileId": "openBadgeCredential",
"authMethod": "PRE_AUTHORIZED",
"expiresAt": 1782909743958,
"credentialOffer": "openid-credential-offer://?credential_offer_uri=http%3A%2F%2Fissuer-api2%3A7005%2Fopenid4vci%2Fcredential-offer%3Fid%3D477ecf7e-e783-4326-85dd-a590e94d06e6"
}
```
**Response Fields**
- **offerId**: _String_ - Unique identifier for this offer.
- **expiresAt**: _Long_ - Unix timestamp (ms) when the offer expires (default: 300 seconds after creation).
- **credentialOffer**: _String_ - The OID4VCI credential offer URL. Turn this into a QR code or deep link for the wallet.
---
## Claiming the Offer
1. **Generate a QR code** from the `credentialOffer` URL, or use it directly as a deep link
2. **Open the [walt.id Wallet](https://wallet.demo.walt.id/)** and log in
3. **Click "Request Credential"** and paste the offer URL into the field below the camera
4. **The wallet resolves the offer**, requests the credential, and stores it
🎉 You've issued a W3C Verifiable Credential using OID4VCI.
To learn how to accept this credential offer programmatically instead, see [How to Accept W3C VCs via OID4VCI](https://docs.walt.id/community-stack/wallet/credential-exchange/guides/accept-w3c-vc-oid4vci.md).
---
## Defining Your Own Profile
To issue your own W3C VC type instead of the shipped default, two config files need an entry, and the service needs a restart to pick them up.
### Step 1: Declare the Credential Type
Add an entry to `credential-issuer-metadata.conf`:
```hocon
credentialConfigurations = {
"MyCredential_jwt_vc_json" = {
format = "jwt_vc_json"
scope = "MyCredential_jwt_vc_json"
cryptographic_binding_methods_supported = ["jwk", "did:key", "did:web"]
credential_signing_alg_values_supported = ["ES256"]
proof_types_supported = {
jwt = {
proof_signing_alg_values_supported = ["ES256"]
}
}
credential_definition = {
type = ["VerifiableCredential", "MyCredential"]
}
}
}
```
See [Credential Issuer Metadata](https://docs.walt.id/community-stack/issuer2/configurations/config-files/credential-issuer-metadata.md) for the full field reference.
### Step 2: Define a Profile
Add a matching profile to `issuer2-profiles.conf`:
```hocon
profiles {
myCredential {
name = "My Credential"
credentialConfigurationId = "MyCredential_jwt_vc_json"
issuerKey = ${defaultIssuerKey}
issuerDid = ${defaultIssuerDid}
credentialData = {
"@context" = ["https://www.w3.org/2018/credentials/v1"]
type = ["VerifiableCredential", "MyCredential"]
credentialSubject = {
name = "Jane Smith"
}
}
mapping = {
id = ""
issuer = { id = "" }
credentialSubject = { id = "" }
issuanceDate = ""
expirationDate = ""
}
}
}
```
See [Issuer Profiles Configuration](https://docs.walt.id/community-stack/issuer2/configurations/config-files/issuer-profiles.md) for every available profile field, including notifications and credential status.
### Step 3: Restart and Verify
**Error:**
Profiles are loaded once, at startup. **Restart the Issuer2 container** after editing either config file, then confirm the new profile is loaded.
```bash
curl -X 'GET' 'http://localhost:7005/issuer2/profiles/myCredential'
```
Then repeat the two steps from [Quick Start](#quick-start-issue-using-the-default-profile) above, substituting your own `profileId`.
---
## Using the Authorization Code Flow Instead
The example above used `PRE_AUTHORIZED` — no user login required. Issuer2 also supports the **Authorization Code Flow**, where the holder authenticates against an external IdP (e.g. Keycloak) before claiming the credential, and the resulting ID token claims are mapped into the credential via `idTokenClaimsMapping`.
This requires registering an external OAuth 2.0 / OpenID Connect (OIDC) provider first. See [Authorization Code Flow](https://docs.walt.id/community-stack/issuer2/protocol-flows/authorization-code-flow.md) for the full sequence and setup.
## Credential Status (Revocation)
To make a W3C VC revocable, embed a Bitstring Status List reference in the profile. See [Credential Status](https://docs.walt.id/community-stack/issuer2/credential-offers/credential-status.md) for the full reference.
---
## Next Steps
- **Verify the credential you just issued** — Try [Verifying W3C VCs](https://docs.walt.id/community-stack/verifier2/credential-verification/vc-oid4vp.md) with the walt.id Verifier2 API
- **Populate credentials dynamically** — See [Data Functions](https://docs.walt.id/community-stack/issuer2/data-functions.md) for webhooks, timestamps, and more
- **Track issuance events** — See [Notifications](https://docs.walt.id/community-stack/issuer2/notifications.md) for webhook and SSE options
- **Issue other credential types** — Try [SD-JWT VC](https://docs.walt.id/community-stack/issuer2/credential-issuance/sd-jwt-vc.md) or [mDL](https://docs.walt.id/community-stack/issuer2/credential-issuance/mdl.md)
--------------------------------------------------------------------------------
PAGE: Issuing Mobile Driver's Licenses (mDL) via OID4VCI
URL: https://docs.walt.id/community-stack/issuer2/credential-issuance/mdl
RAW: https://docs.walt.id/community-stack/issuer2/credential-issuance/mdl.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Issuing Mobile Driver's Licenses (mDL) via OID4VCI
Video: https://youtu.be/_Fd88f9OiGc
This guide shows you how to issue an [ISO/IEC 18013-5](https://docs.walt.id/concepts/digital-credentials/mdoc-mdl-iso.md) Mobile Driver's License (mDL) using the walt.id Issuer2 API. You'll issue a credential from a ready-made profile in seconds, then see how to define your own.
[**mDL (Mobile Driver's License)**](https://docs.walt.id/concepts/digital-credentials/mdoc-mdl-iso.md): A digital representation of a driver's license based on the ISO/IEC 18013-5 standard, stored securely on a digital ID wallet.
[**OID4VCI**](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md): The protocol used to deliver the credential to a wallet (mDoc issuance follows OID4VCI per ISO 18013-7).
**Note:**
Issuer2 issues credentials from **profiles** — reusable configurations declared ahead of time in a config file, with sensible defaults for the issuer key and credential data. If you're used to the original Issuer (v1), where the key and credential data were posted directly to an issuance endpoint every time, see [Getting Started](https://docs.walt.id/community-stack/issuer2/getting-started.md#core-concepts) for what changed — you can still override a profile's defaults per-offer via [`runtimeOverrides`](https://docs.walt.id/community-stack/issuer2/credential-offers/create-offer.md#runtime-overrides).
## Prerequisites
Before you begin, ensure you have:
- **Issuer2 API running** — Follow the [Setup](https://docs.walt.id/community-stack/issuer2/setup.md) guide
- **A test wallet** — Use the [walt.id Wallet](https://wallet.demo.walt.id/) for testing, or any OID4VCI-compatible wallet with mDoc support
**Note:**
**Local Development:** When running locally via Docker, the API is available at `http://localhost:7005` by default, whether standalone or via Docker Compose. Use this as your base URL in the examples below.
---
## Quick Start: Issue Using the Default Profile
The waltid-identity repository ships with a ready-made profile called `isoMdl`, bound to the `org.iso.18013.5.1.mDL` credential type. You can issue from it immediately — no configuration file editing required.
### Step 1: Confirm the Profile Is Loaded
**Option: CURL**
**Endpoint:** `GET /issuer2/profiles/{profileId}` | [API Reference](https://issuer2.demo.walt.id/swagger/index.html)
##### Example Request
```bash
curl -X 'GET' \
'http://localhost:7005/issuer2/profiles/isoMdl' \
-H 'accept: application/json'
```
---
##### Example Response
```json
{
"profileId": "isoMdl",
"name": "ISO 18013-5 Mobile Driving License",
"credentialConfigurationId": "org.iso.18013.5.1.mDL",
"issuerKey": {
"type": "jwk",
"jwk": {
"d": "KJ4k3Vcl5Sj9Mfq4rrNXBm2MoPoY3_Ak_PIR_EgsFhQ",
"crv": "P-256",
"x": "G0RINBiF-oQUD3d5DGnegQuXenI29JDaMGoMvioKRBM",
"y": "ed3eFGs2pEtrp7vAZ7BLcbrUtpKkYWAT2JPUQK4lN4E",
"kty": "EC"
}
},
"credentialData": {
"org.iso.18013.5.1": {
"un_distinguishing_sign": "A",
"biometric_template_signature_sign": null,
"portrait_capture_date": "2024-12-20",
"age_over_16": true,
"age_over_18": true,
"given_name_national_character": "Anna Maria",
"given_name": "Anna Maria",
"birth_place": "Graz",
"family_name": "Musterfrau",
"age_birth_year": 1988,
"age_over_12": true,
"nationality": "AUT",
"age_over_13": true,
"age_over_14": true,
"weight": 63,
"issuing_jurisdiction": "AT-9",
"expiry_date": "2035-01-15",
"signature_usual_mark": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO7Z0i8AAAAASUVORK5CYII=",
"biometric_template_finger": null,
"eye_colour": "brown",
"resident_postal_code": "1070",
"sex": 2,
"age_over_62": false,
"issuing_country": "AT",
"hair_colour": "brown",
"issue_date": "2025-01-15",
"age_over_60": false,
"age_in_years": 36,
"administrative_number": "AT-FS-99817234",
"age_over_65": false,
"resident_state": "Wien",
"resident_city": "Wien",
"document_number": "DL-AT-2025-00018427",
"portrait": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO7Z0i8AAAAASUVORK5CYII=",
"resident_country": "AT",
"resident_address": "Mariahilfer Straße 120/8",
"age_over_68": false,
"driving_privileges": [
{
"issue_date": "2006-09-14",
"vehicle_category_code": "B",
"expiry_date": "2035-01-15"
},
{
"issue_date": "2004-08-25",
"vehicle_category_code": "AM",
"expiry_date": "2035-01-15"
}
],
"age_over_21": true,
"height": 170,
"biometric_template_iris": null,
"birth_date": "1988-08-25",
"age_over_25": true,
"biometric_template_face": null,
"issuing_authority": "Bundesministerium für Inneres",
"family_name_national_character": "Musterfrau"
}
},
"idTokenClaimsMapping": {
"$.family_name": "$.['org.iso.18013.5.1'].family_name",
"$.given_name": "$.['org.iso.18013.5.1'].given_name"
},
"mDocNameSpacesDataMappingConfig": {
"org.iso.18013.5.1": {
"entriesConfigMap": {
"portrait_capture_date": {
"type": "string",
"conversionType": "stringToFullDate"
},
"driving_privileges": {
"type": "array",
"arrayConfig": [
{
"type": "object",
"entriesConfigMap": {
"issue_date": {
"type": "string",
"conversionType": "stringToFullDate"
},
"expiry_date": {
"type": "string",
"conversionType": "stringToFullDate"
}
}
},
{
"type": "object",
"entriesConfigMap": {
"issue_date": {
"type": "string",
"conversionType": "stringToFullDate"
},
"expiry_date": {
"type": "string",
"conversionType": "stringToFullDate"
}
}
}
]
},
"issue_date": {
"type": "string",
"conversionType": "stringToFullDate"
},
"portrait": {
"type": "string",
"conversionType": "base64StringToByteString"
},
"birth_date": {
"type": "string",
"conversionType": "stringToFullDate"
},
"expiry_date": {
"type": "string",
"conversionType": "stringToFullDate"
},
"signature_usual_mark": {
"type": "string",
"conversionType": "base64StringToByteString"
}
}
}
},
"x5Chain": [
"-----BEGIN CERTIFICATE-----\nMIIBeTCCAR8CFHrWgrGl5KdefSvRQhR+aoqdf48+MAoGCCqGSM49BAMCMBcxFTATBgNVBAMMDE1ET0MgUk9PVCBDQTAgFw0yNTA1MTQxNDA4MDlaGA8yMDc1MDUwMjE0MDgwOVowZTELMAkGA1UEBhMCQVQxDzANBgNVBAgMBlZpZW5uYTEPMA0GA1UEBwwGVmllbm5hMRAwDgYDVQQKDAd3YWx0LmlkMRAwDgYDVQQLDAd3YWx0LmlkMRAwDgYDVQQDDAd3YWx0LmlzMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEG0RINBiF+oQUD3d5DGnegQuXenI29JDaMGoMvioKRBN53d4UazakS2unu8BnsEtxutS2kqRhYBPYk9RAriU3gTAKBggqhkjOPQQDAgNIADBFAiAOMwM7hH7q9Di+mT6qCi4LvB+kH8OxMheIrZ2eRPxtDQIhALHzTxwvN8Udt0Z2Cpo8JBihqacfeXkIxVAO8XkxmXhB\n-----END CERTIFICATE-----"
]
}
```
The profile already ships with `x5Chain` (for CBOR signing) and `mDocNameSpacesDataMappingConfig` (to encode dates and images to the correct CBOR types) pre-configured — you don't need to understand every field yet to issue your first credential. We'll come back to them in [Defining Your Own Profile](#defining-your-own-profile) below.
### Step 2: Create a Credential Offer
**Option: CURL**
**Endpoint:** `POST /issuer2/credential-offers` | [API Reference](https://issuer2.demo.walt.id/swagger/index.html)
##### Example Request
```bash
curl -X 'POST' \
'http://localhost:7005/issuer2/credential-offers' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"profileId": "isoMdl",
"authMethod": "PRE_AUTHORIZED"
}'
```
**Body Parameters**
- **profileId**: _String_ (required) - The profile to issue from. Here, the shipped default `isoMdl`.
- **authMethod**: _String_ (required) - The OID4VCI flow used to claim the offer. `PRE_AUTHORIZED` requires no user login and is the simplest way to get started. See [Using the Authorization Code Flow Instead](#using-the-authorization-code-flow-instead) for the alternative.
---
##### Example Response
```json
{
"offerId": "99825a89-73d0-47f4-80cb-321fea52cd54",
"profileId": "isoMdl",
"authMethod": "PRE_AUTHORIZED",
"expiresAt": 1782910641406,
"credentialOffer": "openid-credential-offer://?credential_offer_uri=http%3A%2F%2Fissuer-api2%3A7005%2Fopenid4vci%2Fcredential-offer%3Fid%3D99825a89-73d0-47f4-80cb-321fea52cd54"
}
```
**Response Fields**
- **offerId**: _String_ - Unique identifier for this offer.
- **expiresAt**: _Long_ - Unix timestamp (ms) when the offer expires (default: 300 seconds after creation).
- **credentialOffer**: _String_ - The OID4VCI credential offer URL. Turn this into a QR code or deep link for the wallet.
---
## Claiming the Offer
1. **Generate a QR code** from the `credentialOffer` URL, or use it directly as a deep link
2. **Open the [walt.id Wallet](https://wallet.demo.walt.id/)** and log in
3. **Click "Request Credential"** and paste the offer URL into the field below the camera
4. **The wallet resolves the offer**, requests the credential, and stores it
🎉 You've issued a Mobile Driver's License using OID4VCI.
To learn how to accept this credential offer programmatically instead, see [How to Accept mDL Credentials via OID4VCI](https://docs.walt.id/community-stack/wallet/credential-exchange/guides/accept-iso-18013-5-mdl-oid4vci.md).
---
## Defining Your Own Profile
To issue your own mDoc credential type instead of the shipped default, two config files need an entry, and the service needs a restart to pick them up.
### Step 1: Declare the Credential Type
Add an entry to `credential-issuer-metadata.conf`:
```hocon
credentialConfigurations = {
"org.iso.18013.5.1.mDL.custom" = {
format = "mso_mdoc"
scope = "org.iso.18013.5.1.mDL.custom"
doctype = "org.iso.18013.5.1.mDL"
cryptographic_binding_methods_supported = ["cose_key"]
credential_signing_alg_values_supported = [-7, -9]
proof_types_supported = {
jwt = {
proof_signing_alg_values_supported = ["ES256"]
}
}
}
}
```
See [Credential Issuer Metadata](https://docs.walt.id/community-stack/issuer2/configurations/config-files/credential-issuer-metadata.md) for the full field reference.
### Step 2: Define a Profile
Add a matching profile to `issuer2-profiles.conf`. Note that mDoc profiles sign with `x5Chain` instead of `issuerDid`, and need `mDocNameSpacesDataMappingConfig` for any date or binary field (see [why this is needed](https://docs.walt.id/community-stack/issuer2/configurations/config-files/issuer-profiles.md#why-this-is-needed)):
```hocon
profiles {
myMdl {
name = "My mDL"
credentialConfigurationId = "org.iso.18013.5.1.mDL.custom"
issuerKey = ${defaultIssuerKey}
x5Chain = ${defaultIssuerX5chain}
credentialData = {
"org.iso.18013.5.1" = {
family_name = "Doe"
given_name = "John"
birth_date = "1990-01-01"
issue_date = "2024-01-01"
expiry_date = "2029-01-01"
}
}
mDocNameSpacesDataMappingConfig = {
"org.iso.18013.5.1" = {
entriesConfigMap = {
birth_date = { type = "string", conversionType = "stringToFullDate" }
issue_date = { type = "string", conversionType = "stringToFullDate" }
expiry_date = { type = "string", conversionType = "stringToFullDate" }
}
}
}
}
}
```
See [Issuer Profiles Configuration](https://docs.walt.id/community-stack/issuer2/configurations/config-files/issuer-profiles.md) for every available profile field, including notifications and credential status.
### Step 3: Restart and Verify
**Error:**
Profiles are loaded once, at startup. **Restart the Issuer2 container** after editing either config file, then confirm the new profile is loaded.
```bash
curl -X 'GET' 'http://localhost:7005/issuer2/profiles/myMdl'
```
Then repeat the two steps from [Quick Start](#quick-start-issue-using-the-default-profile) above, substituting your own `profileId`.
---
## Using the Authorization Code Flow Instead
The example above used `PRE_AUTHORIZED` — no user login required. Issuer2 also supports the **Authorization Code Flow**, where the holder authenticates against an external IdP (e.g. Keycloak) before claiming the credential, and the resulting ID token claims are mapped into the credential via `idTokenClaimsMapping`.
This requires registering an external OAuth 2.0 / OpenID Connect (OIDC) provider first. See [Authorization Code Flow](https://docs.walt.id/community-stack/issuer2/protocol-flows/authorization-code-flow.md) for the full sequence and setup.
## mDoc Data Type Mapping
CBOR requires specific types for dates and binary data (e.g. `portrait`) that JSON can't represent natively. See [mDocNameSpacesDataMappingConfig](https://docs.walt.id/community-stack/issuer2/configurations/config-files/issuer-profiles.md#mdocnamespacesdatamappingconfig) for the full conversion reference.
---
## Next Steps
- **Verify the credential you just issued** — Try [Verifying mDLs](https://docs.walt.id/community-stack/verifier2/credential-verification/mdl-oid4vp.md) with the walt.id Verifier2 API
- **Populate credentials dynamically** — See [Data Functions](https://docs.walt.id/community-stack/issuer2/data-functions.md) for webhooks, timestamps, and more
- **Track issuance events** — See [Notifications](https://docs.walt.id/community-stack/issuer2/notifications.md) for webhook and SSE options
- **Issue other credential types** — Try [SD-JWT VC](https://docs.walt.id/community-stack/issuer2/credential-issuance/sd-jwt-vc.md) or [W3C VC](https://docs.walt.id/community-stack/issuer2/credential-issuance/w3c-vc.md)
--------------------------------------------------------------------------------
PAGE: Overview
URL: https://docs.walt.id/community-stack/issuer2/credential-profiles/overview
RAW: https://docs.walt.id/community-stack/issuer2/credential-profiles/overview.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Overview
**Credential Profiles** are the foundation of the Issuer2 API's profile-based architecture. A profile defines all the default values needed to issue a specific type of credential, making it easy to create consistent credential offers without repeating configuration.
## What is a Credential Profile?
A credential profile is a reusable configuration that contains:
- **Credential Configuration ID** – The type of credential this profile issues (must match a supported credential type in your issuer metadata)
- **Issuer Identity** – The DID, key, and/or X.509 certificate chain used to sign credentials
- **Credential Data** – Default data template for the credential
- **Data Mapping** – Dynamic data functions for populating credential fields
- **ID Token Claims Mapping** – Mapping from external IdP claims to credential data (for authorization code flow)
- **mDoc Data Mapping** – Type conversions for mDoc/mDL credential fields
- **Selective Disclosure** – Selective disclosure configuration for SD-JWT credentials
- **Notifications** – Webhook settings for issuance events
Profiles are defined in the `issuer2-profiles.conf` configuration file and loaded when the service starts. For the full field reference, examples for every credential format, and how to write and reload the file, see [Issuer Profiles Configuration](https://docs.walt.id/community-stack/issuer2/configurations/config-files/issuer-profiles.md).
**Note:**
The waltid-identity repository ships with a set of ready-to-use default profiles — you don't need to write your own to issue your first credential. See [Credential Issuance](https://docs.walt.id/community-stack/issuer2/credential-issuance/sd-jwt-vc.md) to try one immediately.
## Supported Credential Formats
Profiles can be created for any credential format supported:
| Format | Description |
|--------|-------------|
| `dc+sd-jwt` | IETF SD-JWT Verifiable Credentials |
| `jwt_vc_json` | W3C Verifiable Credentials (JWT) |
| `mso_mdoc` | ISO 18013-5 Mobile Documents (mDL) |
## Reading Profiles via the API
Once configured, profiles are available to read (but not create or edit) via the API:
### List All Profiles
```bash
curl -X GET 'http://localhost:7005/issuer2/profiles'
```
### Get a Specific Profile
```bash
curl -X GET 'http://localhost:7005/issuer2/profiles/{profileId}'
```
Use this to confirm a profile you added to `issuer2-profiles.conf` was picked up correctly after restarting the service.
## Next Steps
- [Issue a Credential](https://docs.walt.id/community-stack/issuer2/credential-issuance/sd-jwt-vc.md) – Try issuing a credential using a ready-made default profile
- [Issuer Profiles Configuration](https://docs.walt.id/community-stack/issuer2/configurations/config-files/issuer-profiles.md) – The full field reference for writing your own profiles
- [Credential Offers](https://docs.walt.id/community-stack/issuer2/credential-offers/overview.md) – Create offers from your profiles
--------------------------------------------------------------------------------
PAGE: Overview
URL: https://docs.walt.id/community-stack/issuer2/credential-offers/overview
RAW: https://docs.walt.id/community-stack/issuer2/credential-offers/overview.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Overview
Before a wallet can receive a credential, the issuer creates a credential offer from a [credential profile](https://docs.walt.id/community-stack/issuer2/credential-profiles/overview.md). The Issuer2 API exposes endpoints for creating and managing these offers, which generate an [OID4VCI](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md) credential offer URL that any compliant wallet can claim.
## What is a Credential Offer?
A credential offer is a one-time or limited-use invitation for a wallet to receive a credential. When you create an offer, you specify:
- **Profile** – The credential profile to use as the base configuration
- **Authentication Method** – Pre-authorized or authorization code flow
- **Delivery Mode** – Credential offer by reference or by value
- **Expiration** – How long the offer remains valid
- **Runtime Overrides** – Any profile values to override for this specific offer
## Offer Flow
1. **Create Offer** – Call the offers endpoint with a profile ID
2. **Share URL** – Send the credential offer URL to the user (QR code, deep link, etc.)
3. **Wallet Claims** – User's wallet resolves the offer and receives the credential
## Authentication Methods
| Method | Description | Use Case |
|--------|-------------|----------|
| `PRE_AUTHORIZED` | No user authentication required | Known users, pre-verified data |
| `AUTHORIZED` | User must authenticate via IdP | Unknown users, dynamic data collection |
See [Protocol Flows](https://docs.walt.id/community-stack/issuer2/protocol-flows/overview.md) for the full sequence diagrams and a more detailed breakdown of when to use each.
## Offer Delivery Modes
| Mode | Description |
|------|-------------|
| `BY_REFERENCE` | Offer URL contains a reference; wallet fetches full offer from issuer (default) |
| `BY_VALUE` | Full credential offer is embedded in the URL |
**Note:**
`BY_REFERENCE` is recommended for most use cases as it keeps URLs shorter and allows the issuer to track when offers are resolved.
## API Endpoints
### Create a Credential Offer
```bash
curl -X POST 'http://localhost:7005/issuer2/credential-offers' \
-H 'Content-Type: application/json' \
-d '{
"profileId": "universityDegree",
"authMethod": "PRE_AUTHORIZED"
}'
```
**Response:**
```json
{
"offerId": "abc123-def456-ghi789",
"profileId": "universityDegree",
"authMethod": "PRE_AUTHORIZED",
"expiresAt": 1704067500000,
"credentialOffer": "openid-credential-offer://?credential_offer_uri=http%3A%2F%2Flocalhost%3A7005%2Fopenid4vci%2Fcredential-offer%3Fid%3Dabc123-def456-ghi789"
}
```
### List Sessions
```bash
curl -X GET 'http://localhost:7005/issuer2/sessions'
```
### Get Session Details
```bash
curl -X GET 'http://localhost:7005/issuer2/sessions/{sessionId}'
```
## Using the Credential Offer
The `credentialOffer` URL in the response can be:
1. **Displayed as QR Code** – User scans with their wallet app
2. **Sent as Deep Link** – User clicks link on mobile device
3. **Embedded in Email/Message** – User clicks to open in wallet
When the wallet resolves this URL, it will:
1. Fetch the full credential offer from the issuer
2. Display the credential details to the user
3. Request the credential using the appropriate flow
## Next Steps
- [Create an Offer](https://docs.walt.id/community-stack/issuer2/credential-offers/create-offer.md) – Step-by-step guide to creating credential offers
- [Protocol Flows](https://docs.walt.id/community-stack/issuer2/protocol-flows/overview.md) – Full sequence diagrams for both authentication methods
- [Notifications](https://docs.walt.id/community-stack/issuer2/notifications.md) – Monitor issuance progress via webhooks or SSE
--------------------------------------------------------------------------------
PAGE: Create an Offer
URL: https://docs.walt.id/community-stack/issuer2/credential-offers/create-offer
RAW: https://docs.walt.id/community-stack/issuer2/credential-offers/create-offer.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Create a Credential Offer
This guide walks you through creating a credential offer from an existing profile. The offer generates an [OID4VCI](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md) credential offer URL that can be claimed by any compliant wallet.
## Prerequisites
Before creating an offer, ensure you have:
1. [Issuer2 API running](https://docs.walt.id/community-stack/issuer2/setup.md)
2. [Credential Profile configured](https://docs.walt.id/community-stack/issuer2/configurations/config-files/issuer-profiles.md) — or use one of the [ready-made default profiles](https://docs.walt.id/community-stack/issuer2/credential-issuance/sd-jwt-vc.md)
## Create an Offer
**Option: CURL**
**Endpoint:** `POST /issuer2/credential-offers` | [API Reference](https://issuer2.demo.walt.id/swagger/index.html)
##### Example Request
```bash
curl -X 'POST' \
'http://localhost:7005/issuer2/credential-offers' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"profileId": "universityDegree",
"authMethod": "PRE_AUTHORIZED"
}'
```
**Body Parameters**
- **profileId**: _String_ (required) - The ID of the credential profile to use.
- **authMethod**: _String_ (required) - Authentication method: `PRE_AUTHORIZED` or `AUTHORIZED`.
- **valueMode**: _String_ (optional) - How the credential offer is delivered (default: `BY_REFERENCE`). `BY_REFERENCE` — offer URL contains a reference, wallet fetches the full offer. `BY_VALUE` — full credential offer embedded in the URL.
- **issuerStateMode**: _String_ (optional) - Whether to include issuer state for `AUTHORIZED` offers (default: `INCLUDE`). `OMIT` — no issuer state. `INCLUDE` — include issuer state for session correlation with authorization servers.
- **expiresInSeconds**: _Integer_ (optional) - Offer validity duration in seconds (default: 300). Set to `-1` for no expiration.
- **txCode**: _Object_ (optional) - Transaction code (PIN) configuration for pre-authorized flow. See [Transaction Code Configuration](#transaction-code-configuration).
- **txCodeValue**: _String_ (optional) - Specific PIN value (if not provided, one is generated).
- **runtimeOverrides**: _Object_ (optional) - Override profile values for this offer. See [Runtime Overrides](#runtime-overrides).
- **sessionId**: _String_ (optional) - Session ID for the offer. If not provided, a random session ID will be generated.
---
##### Example Response
```json
{
"offerId": "abc123-def456-ghi789",
"profileId": "universityDegree",
"authMethod": "PRE_AUTHORIZED",
"expiresAt": 1704067500000,
"credentialOffer": "openid-credential-offer://?credential_offer_uri=http%3A%2F%2Flocalhost%3A7005%2Fopenid4vci%2Fcredential-offer%3Fid%3Dabc123-def456-ghi789"
}
```
**Response Fields**
- **offerId**: _String_ - Unique identifier for this offer.
- **profileId**: _String_ - The profile used to create this offer.
- **authMethod**: _String_ - The authentication method.
- **issuerStateMode**: _String_ - The issuer state mode (for authorized flow).
- **expiresAt**: _Long_ - Unix timestamp (ms) when the offer expires.
- **txCodeValue**: _String_ - The PIN value (only for pre-authorized flow with txCode).
- **credentialOffer**: _String_ - The OID4VCI credential offer URL.
## Examples
### Pre-Authorized Flow with PIN
Require a PIN for additional security:
```bash
curl -X POST 'http://localhost:7005/issuer2/credential-offers' \
-H 'Content-Type: application/json' \
-d '{
"profileId": "universityDegree",
"authMethod": "PRE_AUTHORIZED",
"txCode": {
"input_mode": "numeric",
"length": 6,
"description": "Please enter the PIN sent to your email"
}
}'
```
**Response includes the PIN:**
```json
{
"offerId": "abc123-def456-ghi789",
"profileId": "universityDegree",
"authMethod": "PRE_AUTHORIZED",
"expiresAt": 1704067500000,
"txCodeValue": "123456",
"credentialOffer": "openid-credential-offer://..."
}
```
### Pre-Authorized Flow with Custom PIN
Specify your own PIN value:
```bash
curl -X POST 'http://localhost:7005/issuer2/credential-offers' \
-H 'Content-Type: application/json' \
-d '{
"profileId": "universityDegree",
"authMethod": "PRE_AUTHORIZED",
"txCode": {
"input_mode": "numeric",
"length": 4
},
"txCodeValue": "1234"
}'
```
### Authorization Code Flow
For user authentication via external IdP:
```bash
curl -X POST 'http://localhost:7005/issuer2/credential-offers' \
-H 'Content-Type: application/json' \
-d '{
"profileId": "identityCredentialSdJwt",
"authMethod": "AUTHORIZED",
"issuerStateMode": "INCLUDE"
}'
```
`issuerStateMode: "INCLUDE"` lets the Issuer2 API correlate the authorization request it receives back with this offer's issuance session. See [Authorization Code Flow](https://docs.walt.id/community-stack/issuer2/protocol-flows/authorization-code-flow.md) for the full sequence, including how (and when) `issuer_state` is forwarded to the external authorization server.
**Note:**
`issuer_state` can be viewed as a mechanism in OpenID4VCI for correlating a Credential Offer with the later Authorization Request in the Authorization Code Flow. The issuer creates it, the wallet treats it as opaque, and if it is present in the offer the wallet sends it back in the Authorization Request. Issuer2 includes `issuer_state` by default for `AUTHORIZED` offers. If you explicitly set `issuerStateMode` to `OMIT`, do not use `runtimeOverrides`; offer-specific overrides require issuer-state correlation and the request is rejected.
**Note:**
You can run this example without setting up an identity provider. Out of the box it uses a [hosted walt.id test provider](https://docs.walt.id/community-stack/issuer2/configurations/config-files/authentication-service.md#default-test-provider) — open the offer in a wallet and log in as `jane@walt.id` / `jane`. Because the `identityCredentialSdJwt` profile maps `given_name` and `family_name` from the ID token, the issued credential is populated with Jane's name from her login.
### Offer by Value
Embed the full offer in the URL (useful for offline scenarios):
```bash
curl -X POST 'http://localhost:7005/issuer2/credential-offers' \
-H 'Content-Type: application/json' \
-d '{
"profileId": "universityDegree",
"authMethod": "PRE_AUTHORIZED",
"valueMode": "BY_VALUE"
}'
```
### Custom Expiration
Set a longer expiration time:
```bash
curl -X POST 'http://localhost:7005/issuer2/credential-offers' \
-H 'Content-Type: application/json' \
-d '{
"profileId": "universityDegree",
"authMethod": "PRE_AUTHORIZED",
"expiresInSeconds": 3600
}'
```
### Runtime Overrides
Override profile values for a specific offer — including the signing key, not just the credential data:
```bash
curl -X POST 'http://localhost:7005/issuer2/credential-offers' \
-H 'Content-Type: application/json' \
-d '{
"profileId": "universityDegree",
"authMethod": "PRE_AUTHORIZED",
"runtimeOverrides": {
"issuerKey": {
"type": "jwk",
"jwk": {
"kty": "OKP",
"d": "5bx6BJCRTtKmXacRtnhjQ6i07UWaAWRixZvFD3t9f00",
"crv": "Ed25519",
"kid": "N4dxEvjiNg9RwEqySdHm2VTjfB0M0JyO9UV5_SS9sQE",
"x": "u2fhaRWUv-Uo24he12WLHTbcdJHWis7VCBA8PJkB0o0"
}
},
"issuerDid": "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoiTjRkeEV2amlOZzlSd0VxeVNkSG0yVlRqZkIwTTBKeU85VVY1X1NTOXNRRSIsIngiOiJ1MmZoYVJXVXYtVW8yNGhlMTJXTEhUYmNkSkhXaXM3VkNCQThQSmtCMG8wIn0",
"credentialData": {
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://www.w3.org/2018/credentials/examples/v1"
],
"type": ["VerifiableCredential", "UniversityDegree"],
"credentialSubject": {
"degree": {
"type": "MasterDegree",
"name": "Master of Computer Science"
}
}
},
"notifications": {
"webhook": {
"url": "https://my-server.com/webhook/special-offer"
}
}
}
}'
```
**Note:**
`issuerKey` (and `issuerDid`/`x5Chain`) override the profile's default signing key for this offer only — useful for issuing a one-off credential with a different key without touching the profile itself. Provide both together, since a mismatched key/DID pair will fail to sign.
### Available Override Fields
| Field | Description |
|-----------------------------------|-------------------------------------------|
| `issuerDid` | Override the issuer DID |
| `issuerKey` | Override the signing key |
| `x5Chain` | Override the X.509 certificate chain |
| `credentialData` | Override the credential data |
| `mapping` | Override the data mapping |
| `selectiveDisclosure` | Override the SD-JWT selective disclosure configuration |
| `idTokenClaimsMapping` | Override ID token claims mapping |
| `mDocNameSpacesDataMappingConfig` | Override mDoc data mapping |
| `authorizedTransactionDataTypes` | Override mDoc OpenID4VP transaction-data types authorized in the MSO `KeyAuthorizations` |
| `credentialStatus` | Override credential status configuration |
| `notifications` | Override notification settings |
## Transaction Code Configuration
The `txCode` object configures PIN requirements:
| Property | Type | Description |
|----------|------|-------------|
| `input_mode` | String | Input type: `numeric` or `text` |
| `length` | Integer | Length of the PIN |
| `description` | String | Description shown to the user |
## Using the Credential Offer URL
The `credentialOffer` URL can be:
1. **Displayed as QR Code** – User scans with their wallet app
2. **Sent as Deep Link** – User clicks link on mobile device
3. **Embedded in Email/Message** – User clicks to open in wallet
### Example Decoded Offer URL
```
openid-credential-offer://?credential_offer_uri=http://localhost:7005/openid4vci/credential-offer?id=abc123-def456-ghi789
```
## Next Steps
- [Notifications](https://docs.walt.id/community-stack/issuer2/notifications.md) – Configure webhook notifications
- [Data Functions](https://docs.walt.id/community-stack/issuer2/data-functions.md) – Populate credentials with dynamic data
--------------------------------------------------------------------------------
PAGE: Credential Status
URL: https://docs.walt.id/community-stack/issuer2/credential-offers/credential-status
RAW: https://docs.walt.id/community-stack/issuer2/credential-offers/credential-status.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Credential Status
Unlike the [Enterprise Stack](https://docs.walt.id/enterprise-stack/home.md), where we offer a credential status service to allow you to create, manage and serve status lists to enable credential revocation - within the Community Stack you are responsible for hosting and managing the status lists yourself.
However, you are still able to embed the status information within a credential profile or credential offer override like so:
## Profile Configuration
### Bitstring Status List with W3C JWT
```json
{
"profileId": "openBadgeCredential-with-status",
"name": "Open Badge Credential with Revocation",
"credentialConfigurationId": "OpenBadgeCredential_jwt_vc_json",
"issuerKey": { "type": "jwk", ... },
"issuerDid": "did:key:...",
"credentialData": { ... },
"credentialStatus": {
"id": "https://issuer.example.com/status/1#94567",
"type": "BitstringStatusListEntry",
"statusPurpose": "revocation",
"statusListIndex": "94567",
"statusListCredential": "https://issuer.example.com/status/1"
}
}
```
### Token Status List with IETF SD-JWT
```json
{
"profileId": "identity-sdjwt-with-status",
"name": "Identity Credential with Status",
"credentialConfigurationId": "identity_credential",
"issuerKey": { "type": "jwk", "..." },
"issuerDid": "did:key:...",
"credentialData": {
"vct": "https://example.com/identity/1",
"given_name": "John",
"family_name": "Smith",
"birthdate": "1980-01-01"
},
"credentialStatus": {
"status_list": {
"idx": 12345,
"uri": "https://issuer.example.com/status/sdjwt/1"
}
}
}
```
### Token Status List with mDocs
```json
{
"profileId": "mdl-with-status",
"name": "Mobile Driving License with Status",
"credentialConfigurationId": "org.iso.18013.5.1.mDL",
"issuerKey": { "type": "jwk", "..." },
"issuerDid": "did:key:...",
"credentialData": {
"org.iso.18013.5.1": {
"family_name": "Smith",
"given_name": "John",
"birth_date": "1980-01-01"
}
},
"credentialStatus": {
"status_list": {
"idx": 94567,
"uri": "https://issuer.example.com/status/mdoc/1"
}
}
}
```
## Credential Offer Override
### Bitstring Status List with W3C JWT
```json
{
"profileId": "openBadgeCredential",
"authMethod": "PRE_AUTHORIZED",
"runtimeOverrides": {
"credentialStatus": {
"id": "https://issuer.example.com/status/2#12345",
"type": "BitstringStatusListEntry",
"statusPurpose": "suspension",
"statusListIndex": "12345",
"statusListCredential": "https://issuer.example.com/status/2"
}
}
}
```
### Token Status List with IETF SD-JWT
```json
{
"profileId": "identityCredentialSdJwt",
"authMethod": "PRE_AUTHORIZED",
"runtimeOverrides": {
"credentialStatus": {
"status_list": {
"idx": 12345,
"uri": "https://issuer.example.com/status/sdjwt/2"
}
}
}
}
```
### Token Status List with mDocs
```json
{
"profileId": "isoMdl",
"authMethod": "PRE_AUTHORIZED",
"runtimeOverrides": {
"credentialStatus": {
"status_list": {
"idx": 12345,
"uri": "https://issuer.example.com/status/mdoc/2"
}
}
}
}
```
--------------------------------------------------------------------------------
PAGE: Overview
URL: https://docs.walt.id/community-stack/issuer2/protocol-flows/overview
RAW: https://docs.walt.id/community-stack/issuer2/protocol-flows/overview.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Overview
Issuer2 supports two [OID4VCI](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md) flows for claiming a credential offer, set via the `authMethod` field when [creating a credential offer](https://docs.walt.id/community-stack/issuer2/credential-offers/create-offer.md). This section walks through the full sequence for each so you know what's actually happening between the wallet, your issuer, and (for one of them) an external identity provider.
## Which Flow Should I Use?
| | Pre-Authorized Code Flow | Authorization Code Flow |
|---|---|---|
| **`authMethod`** | `PRE_AUTHORIZED` | `AUTHORIZED` |
| **User authentication** | None (optionally a PIN/`txCode`) | Required — user logs in at an external IdP |
| **Best for** | Known users (already logged in) and data already verified before the offer is created | Unknown users, or credential data that should come from the user's own IdP login (e.g. corporate SSO) |
| **Extra setup required** | None | None to try it — a [hosted test provider](https://docs.walt.id/community-stack/issuer2/configurations/config-files/authentication-service.md#default-test-provider) with ready-made logins is used by default. For production, register your own OAuth 2.0 / OpenID Connect (OIDC) provider in [`authentication-service.conf`](https://docs.walt.id/community-stack/issuer2/configurations/config-files/authentication-service.md) |
| **Credential data source** | Fully defined in the profile (or runtime overrides) ahead of time | Profile defaults (or runtime overrides), enriched with claims from the IdP's ID token via `idTokenClaimsMapping` |
## Available Flows
- [Pre-Authorized Code Flow](https://docs.walt.id/community-stack/issuer2/protocol-flows/pre-authorized-flow.md) — Sequence diagram for issuing a credential with no user login.
- [Authorization Code Flow](https://docs.walt.id/community-stack/issuer2/protocol-flows/authorization-code-flow.md) — Sequence diagram for issuing a credential after the holder authenticates against an external IdP.
--------------------------------------------------------------------------------
PAGE: Pre-Authorized Code Flow
URL: https://docs.walt.id/community-stack/issuer2/protocol-flows/pre-authorized-flow
RAW: https://docs.walt.id/community-stack/issuer2/protocol-flows/pre-authorized-flow.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Pre-Authorized Code Flow
This flow covers your application creating a credential offer, and the wallet claiming it via [OID4VCI](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md) with no user login. Set `"authMethod": "PRE_AUTHORIZED"` when [creating the offer](https://docs.walt.id/community-stack/issuer2/credential-offers/create-offer.md) to use it.
**Components involved:**
- Your Application — creates the credential offer and displays the QR code
- Wallet (Holder) — scans the QR code, completes the OID4VCI flow, stores the credential
- Issuer2 API — manages the issuance session, validates the code/PIN, and signs the credential
```mermaid
sequenceDiagram
participant App as Your Application
participant Wallet as Wallet (Holder)
participant Issuer as Issuer2 API
App->>Issuer: POST /issuer2/credential-offers {profileId, authMethod: PRE_AUTHORIZED}
activate Issuer
Issuer->>Issuer: Create issuance session and pre-authorized code (+ optional txCode/PIN)
Issuer-->>App: 201 Created {credentialOffer: "openid-credential-offer://..."}
deactivate Issuer
App->>App: Generate QR code with credential offer URL
App-->>Wallet: Display QR code (openid-credential-offer://...)
Wallet->>Wallet: Scan QR code
Wallet->>Wallet: Parse credential offer URI
Wallet->>Issuer: GET /.well-known/openid-credential-issuer/openid4vci
Issuer-->>Wallet: Issuer metadata
Wallet->>Issuer: POST /token (pre-authorized_code [+ tx_code])
activate Issuer
Issuer->>Issuer: Validate code (+ PIN if configured)
Issuer-->>Wallet: access_token
deactivate Issuer
Wallet->>Issuer: POST /nonce
activate Issuer
Issuer->>Issuer: Generate fresh c_nonce
Issuer-->>Wallet: {c_nonce}
deactivate Issuer
Wallet->>Wallet: Generate proof of possession (incl. sign c_nonce with holder key)
Wallet->>Issuer: POST /credential (access_token, proof)
activate Issuer
Issuer->>Issuer: Resolve profile, apply mapping / data functions
Issuer->>Issuer: Sign credential with issuerKey
Issuer-->>Wallet: 200 OK {credential}
deactivate Issuer
Wallet->>Wallet: Store credential
```
**Note:**
This diagram is a simplified overview focused on how the Issuer2 API behaves. It omits some protocol details (e.g. metadata discovery of the Nonce Endpoint, PKCE, and exact `.well-known` paths). For the complete, normative sequence see the [OpenID for Verifiable Credential Issuance 1.0 specification](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html).
## Related
- [Credential Issuance](https://docs.walt.id/community-stack/issuer2/credential-issuance/sd-jwt-vc.md) — Try this flow end-to-end
- [Create an Offer](https://docs.walt.id/community-stack/issuer2/credential-offers/create-offer.md) — `txCode` and other offer parameters
- [Authorization Code Flow](https://docs.walt.id/community-stack/issuer2/protocol-flows/authorization-code-flow.md) — The alternative flow, when user login is required
--------------------------------------------------------------------------------
PAGE: Authorization Code Flow
URL: https://docs.walt.id/community-stack/issuer2/protocol-flows/authorization-code-flow
RAW: https://docs.walt.id/community-stack/issuer2/protocol-flows/authorization-code-flow.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Authorization Code Flow
This flow covers your application creating a credential offer, the wallet redirecting the holder to an external IdP to authenticate, and the Issuer2 API mapping the returned ID token claims into the credential. Set `"authMethod": "AUTHORIZED"` when [creating the offer](https://docs.walt.id/community-stack/issuer2/credential-offers/create-offer.md) to use it.
**Components involved:**
- Your Application — creates the credential offer and displays the QR code
- Wallet (Holder) — scans the QR code, redirects the holder to log in, completes the flow
- Issuer2 API — manages the issuance session, correlates `issuer_state`, applies `idTokenClaimsMapping`, and signs the credential
- External IdP — authenticates the holder and issues the ID token (registered in [`authentication-service.conf`](https://docs.walt.id/community-stack/issuer2/configurations/config-files/authentication-service.md))
**Note:**
**Trying this out?** You don't need your own identity provider to start. If you haven't configured `authentication-service.conf`, Issuer2 uses a [hosted walt.id test provider](https://docs.walt.id/community-stack/issuer2/configurations/config-files/authentication-service.md#default-test-provider) with ready-made login accounts (e.g. `jane@walt.id` / `jane`), so you can walk through the whole flow immediately.
**Error:**
For production, this flow requires **your own** external OAuth 2.0 / OpenID Connect (OIDC) provider (e.g. Keycloak, Microsoft Entra ID, Auth0) registered in `authentication-service.conf` before you create an `AUTHORIZED` offer. See [Authentication Service Configuration](https://docs.walt.id/community-stack/issuer2/configurations/config-files/authentication-service.md) to set one up.
```mermaid
sequenceDiagram
participant App as Your Application
participant Wallet as Wallet (Holder)
participant Issuer as Issuer2 API
participant IdP as External IdP (e.g. Keycloak)
App->>Issuer: POST /issuer2/credential-offers {profileId, authMethod: AUTHORIZED, issuerStateMode: INCLUDE}
activate Issuer
Issuer->>Issuer: Create issuance session and issuer_state
Issuer-->>App: 201 Created {credentialOffer: "openid-credential-offer://..."}
deactivate Issuer
App->>App: Generate QR code with credential offer URL
App-->>Wallet: Display QR code
Wallet->>Wallet: Scan QR code
Wallet->>Issuer: GET /.well-known/openid-credential-issuer/openid4vci
Issuer-->>Wallet: Issuer metadata
Wallet->>Issuer: GET /authorize (response_type=code, issuer_state)
activate Issuer
Issuer->>Issuer: Resolve issuance session from issuer_state
Issuer-->>Wallet: 302 redirect to IdP authorizeUrl
deactivate Issuer
Wallet-->>IdP: Follow redirect to authorizeUrl
IdP->>IdP: User authenticates
IdP-->>Issuer: Redirect to /openid4vci/external/oauth/callback?code=...&state=...
activate Issuer
Issuer->>IdP: Exchange code at accessTokenUrl (handled transparently by the issuer's OAuth client)
IdP-->>Issuer: id_token (+ access_token)
Issuer->>Issuer: Look up session by external OAuth state, apply idTokenClaimsMapping into credentialData
Issuer-->>Wallet: 302 redirect back to wallet's redirect_uri with issuer's own authorization code
deactivate Issuer
Wallet->>Issuer: POST /token (authorization_code)
Issuer-->>Wallet: access_token
Wallet->>Issuer: POST /nonce
activate Issuer
Issuer->>Issuer: Generate fresh c_nonce
Issuer-->>Wallet: {c_nonce}
deactivate Issuer
Wallet->>Wallet: Generate proof of possession (incl. sign c_nonce with holder key)
Wallet->>Issuer: POST /credential (access_token, proof)
activate Issuer
Issuer->>Issuer: Sign credential using mapped claims
Issuer-->>Wallet: 200 OK {credential}
deactivate Issuer
Wallet->>Wallet: Store credential
```
**Note:**
This diagram is a simplified overview focused on how the Issuer2 API behaves. It omits some protocol details (e.g. metadata discovery of the Nonce Endpoint, PKCE, and the separate authorization-server metadata fetch). For the complete, normative sequence see the [OpenID for Verifiable Credential Issuance 1.0 specification](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html).
## Understanding `issuer_state`
`issuer_state` is how the Issuer2 API correlates the authorization request it receives back at `/authorize` with the issuance session created when the offer was made. Set `"issuerStateMode": "INCLUDE"` when creating the offer to have it included.
By default, `issuer_state` is **not** forwarded to the external IdP — it's only used internally by the issuer. Set `forwardIssuerStateToAuthorizationServer = true` in `authentication-service.conf` only if your IdP integration specifically needs to receive it as a query parameter on the outgoing authorize request.
## Related
- [Authentication Service Configuration](https://docs.walt.id/community-stack/issuer2/configurations/config-files/authentication-service.md) — Register your OAuth/OIDC provider
- [Issuer Profiles — ID Token Claim Mapping](https://docs.walt.id/community-stack/issuer2/configurations/config-files/issuer-profiles.md#id-token-claim-mapping) — Map ID token claims into credential fields
- [Create an Offer](https://docs.walt.id/community-stack/issuer2/credential-offers/create-offer.md) — `issuerStateMode` and other offer parameters
- [Pre-Authorized Code Flow](https://docs.walt.id/community-stack/issuer2/protocol-flows/pre-authorized-flow.md) — The simpler alternative, when user login isn't needed
--------------------------------------------------------------------------------
PAGE: Feature Manager
URL: https://docs.walt.id/community-stack/issuer2/configurations/feature-manager
RAW: https://docs.walt.id/community-stack/issuer2/configurations/feature-manager.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Feature Manager
Use the feature manager to control which Issuer2 API features are enabled at service startup.
This reduces complexity and the risk of misconfigurations.
## Feature Types
The feature manager offers two types: **Base** and **Optional**.
### Base Features
Base features are always enabled and do not require additional configuration in most cases. They provide
critical functionality expected to be available by default.
### Optional Features
Optional features can be enabled or disabled based on your requirements.
1. **Enabled by default**: These features work without additional configuration (e.g. no extra config file needed).
2. **Disabled by default**: These features must be explicitly enabled in `_features.conf`. They may require their
own configuration to function.
**Note:**
Each feature may have zero, one, or multiple configuration files associated with it.
## How the Feature Manager Works
The Feature Manager ensures efficient and error-free service initialisation through the following steps:
1. **Initialisation with Feature Catalogs**: Services are passed one or more Feature Catalogs on startup.
2. **Determination of Active Features**: The initialiser identifies explicitly enabled or disabled features and
resolves the default states of optional features.
3. **Loading Configurations**: Configurations for base features and explicitly enabled optional features are loaded.
**This results in:**
- Only necessary configurations being loaded, reducing complexity and avoiding unused features.
- Minimal required configuration through sensible defaults.
- Invalid configurations being caught during initialisation before they can reach production.
## Feature Configuration
Configure features by updating the `_features.conf` file as
explained [here](https://docs.walt.id/community-stack/issuer2/configurations/config-files/features.md).
--------------------------------------------------------------------------------
PAGE: Features
URL: https://docs.walt.id/community-stack/issuer2/configurations/config-files/features
RAW: https://docs.walt.id/community-stack/issuer2/configurations/config-files/features.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Features
With `_features.conf` you can control which Issuer2 API features are enabled. They are managed via
the [feature manager](https://docs.walt.id/community-stack/issuer2/configurations/feature-manager.md).
## Available Features
### Base Features
Base features are always enabled. They do not need to be listed in `_features.conf`.
- `credential-types` — Credential types available in this issuer instance. Loads
[credential-issuer-metadata.conf](https://docs.walt.id/community-stack/issuer2/configurations/config-files/credential-issuer-metadata.md)
and [issuer2-profiles.conf](https://docs.walt.id/community-stack/issuer2/configurations/config-files/issuer-profiles.md).
- `issuer-service` — Core issuer service implementation. Loads
[issuer-service.conf](https://docs.walt.id/community-stack/issuer2/configurations/config-files/issuer-service.md).
- `authentication-service` — External OAuth 2.0 / OpenID Connect (OIDC) provider used for authorization-code-flow issuance. Loads
[authentication-service.conf](https://docs.walt.id/community-stack/issuer2/configurations/config-files/authentication-service.md).
- `web` — Web server. Loads [web.conf](https://docs.walt.id/community-stack/issuer2/configurations/config-files/web.md).
### Optional Features
- `persistence` — Redis-backed storage for issuance sessions, codes, and tokens. Disabled by default. Loads
[persistence.conf](https://docs.walt.id/community-stack/issuer2/configurations/config-files/persistence.md). When disabled, state is
kept in memory only.
- `dev-mode` — Development mode. Disabled by default. When enabled, loads additional settings from
[dev-mode.conf](https://docs.walt.id/community-stack/issuer2/configurations/config-files/dev-mode.md) (e.g. allows `did:web` resolution
over plain HTTP).
- `debug-endpoints` — Debug endpoints (heap dump, memory, CPU, etc.). Disabled by default.
The remaining features (`httpclient`, `openapi`, `feature-flag-information-endpoint`, `healthchecks`)
are enabled by default and provide standard infrastructure — the HTTP data fetcher, OpenAPI/Swagger, feature-state
and health endpoints, and the OAuth client for authorization-code login. Most deployments leave these as-is.
## Example Setup
```editorconfig[_features.conf]
enabledFeatures = [
persistence,
dev-mode
]
disabledFeatures = [
# ...
]
```
--------------------------------------------------------------------------------
PAGE: Issuer Service
URL: https://docs.walt.id/community-stack/issuer2/configurations/config-files/issuer-service
RAW: https://docs.walt.id/community-stack/issuer2/configurations/config-files/issuer-service.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Issuer Service Configuration
The `issuer-service.conf` file configures core Issuer2 service settings: the service's public URL, the key used to sign OAuth2 tokens, optional credential encryption support, and the Pushed Authorization Request (PAR) policy.
## File Location
```
waltid-services/waltid-issuer-api2/config/issuer-service.conf
```
## Configuration Options
| Property | Type | Required | Default | Description |
|----------|------|----------|---------|-------------|
| `baseUrl` | String | Yes | – | Public URL where the issuer service is reachable. |
| `ciTokenKey` | String | No | Generated secp256r1 JWK | Serialized key used to sign/verify OAuth2 access & refresh tokens. |
| `credentialEncryptionKey` | String | No | Not set | Serialized EC P-256 private key used for OID4VCI Credential Request decryption and Credential Response encryption. |
| `enforcePushedAuthorizationRequests` | Boolean | No | `false` | Require clients to use the Pushed Authorization Request (PAR) endpoint. |
### baseUrl
The public URL where the issuer service is accessible. Every [OID4VCI](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md) endpoint is derived from it:
- The credential issuer base and metadata URLs become `"/openid4vci"`. This is what is written into credential offers and the `.well-known` metadata, so it **must** be the URL wallets can actually reach.
- The external OAuth callback becomes `"/openid4vci/external/oauth/callback"`.
Trailing slashes are trimmed automatically.
```hocon
baseUrl = "http://localhost:7005"
```
When running via Docker Compose, environment interpolation is supported:
```hocon
baseUrl = "http://${SERVICE_HOST}:${ISSUER_API2_PORT}"
```
### ciTokenKey
The key used to sign and verify the OAuth2 access tokens and refresh tokens issued by the service's token endpoint.
The value is a **serialized walt.id key** — the same format used elsewhere in walt.id — provided as a single JSON string. The `type` field selects the key backend and **is required**; omitting it causes startup to fail. Because the value is a string (not a HOCON object), use a triple-quoted HOCON string so the inner quotes do not need escaping:
```hocon
ciTokenKey = """{"type":"jwk","jwk":{"kty":"EC","d":"...","crv":"P-256","x":"...","y":"..."}}"""
```
Supported `type` values include `jwk` (local key), `tse` (HashiCorp Vault Transit), `aws-rest-api` (AWS KMS), `azure-rest-api` (Azure Key Vault) and `oci-rest-api` (Oracle Cloud KMS).
#### Generating a key
The simplest way to get a stable local JWK in exactly the format above is a one-line [Node.js](https://nodejs.org) command. It is identical on macOS, Linux, and Windows (PowerShell or cmd):
```bash
node -e "const{generateKeyPairSync}=require('crypto');const{privateKey}=generateKeyPairSync('ec',{namedCurve:'P-256'});const q=String.fromCharCode(34).repeat(3);console.log('ciTokenKey = '+q+JSON.stringify({type:'jwk',jwk:privateKey.export({format:'jwk'})})+q)"
```
It prints the complete config line — triple-quoted and ready to paste straight into `issuer-service.conf`, for example:
```hocon
ciTokenKey = """{"type":"jwk","jwk":{"kty":"EC","x":"...","y":"...","crv":"P-256","d":"..."}}"""
```
Generate your own — never reuse the example value from these docs.
If you don't have Node installed, you can instead simply omit `ciTokenKey`: the service generates a key automatically on startup. Note the limitation described below.
**Error:**
If omitted, a new secp256r1 JWK is generated on **every** startup. This is fine for local development, but not for production: the signing key changes on each restart, so any tokens issued before a restart can no longer be verified. Always set `ciTokenKey` explicitly in real deployments.
### credentialEncryptionKey
`credentialEncryptionKey` enables [OID4VCI](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md) Credential Request encryption and Credential Response encryption.
Issuer2 supports this profile:
- Key type: EC P-256
- JWE `alg`: `ECDH-ES`
- JWE `enc`: `A128GCM`
- Compression: not supported
When configured, issuer metadata includes `credential_request_encryption` and `credential_response_encryption` with `encryption_required = false`.
Wallets can keep using normal JSON credential requests. Wallets that send an encrypted Credential Request as `application/jwt` and include `credential_response_encryption` receive the Credential Response as an encrypted JWE.
**Error:**
Use a dedicated key for credential encryption. Do not reuse the token signing key or credential signing keys.
```hocon
credentialEncryptionKey = """{"type":"jwk","jwk":{"kty":"EC","crv":"P-256","x":"...","y":"...","d":"..."}}"""
```
### enforcePushedAuthorizationRequests
Controls the OAuth2 Pushed Authorization Request (PAR, RFC 9126) policy for the authorization code flow. Defaults to `false`.
When `true`, clients **must** submit authorization parameters to the PAR (`/par`) endpoint instead of passing them directly to `/authorize`.
Leave this `false` unless you specifically need to mandate PAR.
```hocon
enforcePushedAuthorizationRequests = false
```
## Example Configuration
```hocon
# issuer-service.conf
baseUrl = "http://localhost:7005"
# Optional: provide a stable token signing key (recommended for anything beyond local dev)
ciTokenKey = """{"type":"jwk","jwk":{"kty":"EC","d":"...","crv":"P-256","x":"...","y":"..."}}"""
# Optional: enable OID4VCI Credential Request/Response encryption
credentialEncryptionKey = """{"type":"jwk","jwk":{"kty":"EC","crv":"P-256","x":"...","y":"...","d":"..."}}"""
# Optional: require Pushed Authorization Requests
enforcePushedAuthorizationRequests = false
```
## Production Configuration
For production deployments, ensure:
1. `baseUrl` points to your public HTTPS URL.
2. `ciTokenKey` is set to a securely generated, persistent key (consider a KMS backend such as `aws-rest-api`, `azure-rest-api` or `tse` rather than an inline JWK).
3. If encrypted Credential Requests or encrypted Credential Responses are required, set `credentialEncryptionKey` to a dedicated EC P-256 private key.
```hocon
# Production example
baseUrl = "https://issuer.example.com"
ciTokenKey = """{"type":"jwk","jwk":{"kty":"EC","d":"...","crv":"P-256","x":"...","y":"..."}}"""
credentialEncryptionKey = """{"type":"jwk","jwk":{"kty":"EC","crv":"P-256","x":"...","y":"...","d":"..."}}"""
enforcePushedAuthorizationRequests = true
```
## Related Configuration
These concerns are configured in their own files, **not** in `issuer-service.conf`:
- [Credential Issuer Metadata](https://docs.walt.id/community-stack/issuer2/configurations/config-files/credential-issuer-metadata.md) – OID4VCI metadata configuration (`credential-issuer-metadata.conf`)
- [Issuer Profiles](https://docs.walt.id/community-stack/issuer2/configurations/config-files/issuer-profiles.md) – Credential profiles, including issuer signing keys and DIDs (`issuer2-profiles.conf`)
--------------------------------------------------------------------------------
PAGE: Credential Issuer Metadata
URL: https://docs.walt.id/community-stack/issuer2/configurations/config-files/credential-issuer-metadata
RAW: https://docs.walt.id/community-stack/issuer2/configurations/config-files/credential-issuer-metadata.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Credential Issuer Metadata Configuration
The `credential-issuer-metadata.conf` file defines the [OID4VCI](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md) credential issuer metadata: how the issuer presents itself, and which credential types it supports. Its contents are published at the `/.well-known/openid-credential-issuer/openid4vci` endpoint.
The file has exactly **two top-level keys**: `issuerDisplay` and `credentialConfigurations`. Each is parsed into strongly-typed OpenID4VCI metadata objects at startup, so the structure below is validated — an invalid entry stops the service from loading.
**Note:**
Credential encryption metadata is derived from `credentialEncryptionKey`. Do not configure `credential_request_encryption` or `credential_response_encryption` manually in this file.
## File Location
```
waltid-services/waltid-issuer-api2/config/credential-issuer-metadata.conf
```
## Configuration Options
| Property | Type | Required | Description |
|----------|------|----------|-------------|
| `issuerDisplay` | Array | No | How the issuer (name/logo) appears in wallets. |
| `credentialConfigurations` | Object (map) | No | Supported credential types, keyed by credential configuration ID. |
## issuerDisplay
A list of display entries for the **issuer itself**, typically one per locale. Purely presentational.
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | String | No | Issuer display name. |
| `locale` | String | No | BCP-47 locale, e.g. `"en-US"`. |
| `logo` | Object | No | Issuer logo. |
| `logo.uri` | String | Yes (within `logo`) | Logo URL. Must be non-blank. |
| `logo.alt_text` | String | No | Alternative text for the logo. |
```hocon
issuerDisplay = [
{
name = "Example Issuer"
locale = "en-US"
logo = {
uri = "https://example.com/logo.png"
alt_text = "Example Issuer Logo"
}
}
]
```
## credentialConfigurations
A map of the credential types this issuer supports. **The map key is the credential configuration ID** (e.g. `"OpenBadgeCredential_jwt_vc_json"`). This ID is the contract point with the rest of the system:
- Profiles in [`issuer2-profiles.conf`](https://docs.walt.id/community-stack/issuer2/configurations/config-files/issuer-profiles.md) reference it via `credentialConfigurationId`.
- It is matched against OAuth scopes during issuance.
### Fields
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `format` | String (enum) | **Yes** | Credential format. One of `jwt_vc_json`, `dc+sd-jwt`, `mso_mdoc`. |
| `scope` | String | No | OAuth scope used to request this credential. Conventionally equal to the configuration ID. Must be non-blank if present. |
| `credential_definition` | Object | No | For W3C VC formats: `{ "@context": [...], type: [...] }`. |
| `doctype` | String | No | Document type for `mso_mdoc`, e.g. `org.iso.18013.5.1.mDL`. |
| `vct` | String | No | Verifiable Credential Type for `dc+sd-jwt`. See [VCT handling](#vct-handling). |
| `credential_signing_alg_values_supported` | Array | No | Format-specific (see below). Must be non-empty if present. |
| `cryptographic_binding_methods_supported` | Array | No | Key binding methods. See [co-dependency](#binding-methods-and-proof-types). |
| `proof_types_supported` | Object | No | Wallet key-proof types. See [co-dependency](#binding-methods-and-proof-types). |
| `credential_metadata` | Object | No | Per-credential display and claim descriptions. See [credential_metadata](#credential_metadata). |
| *(any other key)* | — | No | Passed through verbatim into the published metadata as a custom parameter. Must not reuse a standard field name above. |
**Error:**
Only `format` is strictly required by the schema. However, a credential type that wallets can actually request normally also needs `scope`, `cryptographic_binding_methods_supported`, `proof_types_supported` and `credential_signing_alg_values_supported`.
### Binding methods and proof types
`cryptographic_binding_methods_supported` and `proof_types_supported` are **co-dependent**: if you set one, you must set the other. Setting only one stops the service from loading.
**`cryptographic_binding_methods_supported`** accepts:
- `jwk`
- `cose_key`
- `did:` — e.g. `did:key`, `did:jwk`, `did:web`, `did:ebsi`
**`proof_types_supported`** is a map keyed by proof type (typically `jwt`):
```hocon
cryptographic_binding_methods_supported = ["jwk", "did:key", "did:web", "did:jwk"],
proof_types_supported = {
jwt = {
proof_signing_alg_values_supported = ["ES256"]
}
}
```
### Signing algorithms
`credential_signing_alg_values_supported` is validated against the `format`:
| Format | Expected values | Example |
|--------|-----------------|---------|
| `jwt_vc_json`, `dc+sd-jwt` | JOSE algorithm names (strings) | `["ES256"]` |
| `mso_mdoc` | COSE algorithm identifiers (integers), e.g. `-7` = ES256 | `[-7, -9]` |
### VCT handling
For `dc+sd-jwt` credentials, set `vct` to the special value `"vctBaseUrl"` to publish a self-hosted VCT:
- It is replaced at runtime with `/openid4vci/`.
- The issuer serves the corresponding VCT type metadata at `/.well-known/vct/`.
Any other string is published unchanged, so you can also point to an externally hosted VCT, e.g. `vct = "https://example.com/credentials/identity"`.
### credential_metadata
Per-credential display and claim metadata lives **under `credential_metadata`** — not at the top level of the credential configuration.
**Error:**
A `display` (or `claims`) key placed directly on the credential configuration is **not** interpreted as OpenID4VCI display metadata. It is treated as a custom parameter and republished without validation. Put display and claims under `credential_metadata`.
**`credential_metadata.display`** — optional list of display entries (one per locale; locales must be unique):
| Property | Type | Required | Description |
|----------|------|----------|-------------|
| `name` | String | Yes | Credential display name (non-blank). |
| `locale` | String | No | Locale code, e.g. `"en-US"`. |
| `logo` | Object | No | `{ uri, alt_text }` — `uri` required and must include a scheme. |
| `description` | String | No | Description of the credential. |
| `background_color` | String | No | Hex color. |
| `background_image` | Object | No | `{ uri }` — must include a scheme. |
| `text_color` | String | No | Hex color. |
**`credential_metadata.claims`** — optional list of claim descriptions:
| Property | Type | Required | Description |
|----------|------|----------|-------------|
| `path` | Array<String> | Yes | Claim path (non-empty, no blank segments). |
| `mandatory` | Boolean | No | Whether the claim is mandatory. |
| `display` | Array | No | `{ name, locale }` entries; locales must be unique. |
## Credential Configuration Examples
### SD-JWT VC (dc+sd-jwt)
```hocon
"identity_credential" = {
format = "dc+sd-jwt"
scope = "identity_credential"
vct = "vctBaseUrl"
cryptographic_binding_methods_supported = ["jwk", "did:key", "did:web"]
credential_signing_alg_values_supported = ["ES256"]
proof_types_supported = {
jwt = {
proof_signing_alg_values_supported = ["ES256"]
}
}
credential_metadata = {
display = [
{
name = "Identity Credential"
locale = "en-US"
background_color = "#12107c"
text_color = "#FFFFFF"
}
]
}
}
```
### W3C JWT VC (jwt_vc_json)
```hocon
"OpenBadgeCredential_jwt_vc_json" = {
format = "jwt_vc_json"
scope = "OpenBadgeCredential_jwt_vc_json"
cryptographic_binding_methods_supported = ["jwk", "did:key", "did:web", "did:jwk"]
credential_signing_alg_values_supported = ["ES256"]
proof_types_supported = {
jwt = {
proof_signing_alg_values_supported = ["ES256"]
}
}
credential_definition = {
type = ["VerifiableCredential", "OpenBadgeCredential"]
}
credential_metadata = {
display = [
{
name = "Open Badge Credential"
locale = "en-US"
logo = {
uri = "https://w3c-ccg.github.io/vc-ed/plugfest-1-2022/images/JFF_LogoLockup.png"
alt_text = "JFF Logo"
}
background_color = "#1e3a8a"
text_color = "#FFFFFF"
}
]
}
}
```
### mDoc (mso_mdoc)
```hocon
"org.iso.18013.5.1.mDL" = {
format = "mso_mdoc"
scope = "org.iso.18013.5.1.mDL"
doctype = "org.iso.18013.5.1.mDL"
cryptographic_binding_methods_supported = ["cose_key"]
credential_signing_alg_values_supported = [-7, -9] # COSE identifiers, e.g. -7 = ES256
proof_types_supported = {
jwt = {
proof_signing_alg_values_supported = ["ES256"]
}
}
credential_metadata = {
display = [
{
name = "Mobile Driver's License"
locale = "en-US"
logo = {
uri = "https://example.gov/dmv-logo.png"
alt_text = "DMV Logo"
}
}
]
}
}
```
## Example Configuration
```hocon
# credential-issuer-metadata.conf
issuerDisplay = [
{
name = "Jobs for the Future (JFF)"
locale = "en-US"
logo = {
uri = "https://w3c-ccg.github.io/vc-ed/plugfest-1-2022/images/JFF_LogoLockup.png"
alt_text = "JFF Logo"
}
}
]
credentialConfigurations = {
"identity_credential" = {
format = "dc+sd-jwt"
scope = "identity_credential"
vct = "vctBaseUrl"
cryptographic_binding_methods_supported = ["jwk", "did:key", "did:web"]
credential_signing_alg_values_supported = ["ES256"]
proof_types_supported = {
jwt = {
proof_signing_alg_values_supported = ["ES256"]
}
}
credential_metadata = {
display = [
{
name = "Identity Credential"
locale = "en-US"
background_color = "#12107c"
text_color = "#FFFFFF"
}
]
}
}
"OpenBadgeCredential_jwt_vc_json" = {
format = "jwt_vc_json"
scope = "OpenBadgeCredential_jwt_vc_json"
cryptographic_binding_methods_supported = ["jwk", "did:key", "did:web"]
credential_signing_alg_values_supported = ["ES256"]
proof_types_supported = {
jwt = {
proof_signing_alg_values_supported = ["ES256"]
}
}
credential_definition = {
type = ["VerifiableCredential", "OpenBadgeCredential"]
}
credential_metadata = {
display = [
{
name = "Open Badge Credential"
locale = "en-US"
background_color = "#1e3a8a"
text_color = "#FFFFFF"
}
]
}
}
}
```
## Related Configuration
- [Issuer Profiles](https://docs.walt.id/community-stack/issuer2/configurations/config-files/issuer-profiles.md) – Binds a `credentialConfigurationId` from this file to an issuer key and credential data (`issuer2-profiles.conf`)
- [Issuer Service](https://docs.walt.id/community-stack/issuer2/configurations/config-files/issuer-service.md) – Core service configuration, including the `baseUrl` used to build the published metadata
--------------------------------------------------------------------------------
PAGE: Issuer Profiles
URL: https://docs.walt.id/community-stack/issuer2/configurations/config-files/issuer-profiles
RAW: https://docs.walt.id/community-stack/issuer2/configurations/config-files/issuer-profiles.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Issuer Profiles Configuration
The `issuer2-profiles.conf` file defines **credential profiles** — templates that bind a credential type (declared in [`credential-issuer-metadata.conf`](https://docs.walt.id/community-stack/issuer2/configurations/config-files/credential-issuer-metadata.md)) to a concrete signing key, an issuer identifier, and the data that goes into the credential. All of the data defined in the profile can also be later overwritten using runtime overwrites when creating a credential offer.
The file has two parts:
1. **Top-level shared material** — `defaultIssuerKey`, `defaultIssuerDid`, `defaultIssuerX5chain`. These are *not* applied automatically; profiles reuse them through HOCON substitution (see [Shared defaults](#shared-defaults-and-hocon-substitution)).
2. **`profiles`** — a map of named profiles.
Each profile is parsed into a strongly-typed object at startup and validated, so an invalid entry stops the service from loading.
## File Location
```
waltid-services/waltid-issuer-api2/config/issuer2-profiles.conf
```
## Configuration Options
| Property | Type | Required | Description |
|----------|------|----------|-------------|
| `defaultIssuerKey` | Object | No | Shared issuer signing key, referenced by profiles via `${defaultIssuerKey}`. |
| `defaultIssuerDid` | String | No | Shared issuer DID, referenced via `${defaultIssuerDid}`. |
| `defaultIssuerX5chain` | Array<String> | No | Shared X.509 certificate chain (PEM) for mDoc / x5c signing, referenced via `${defaultIssuerX5chain}`. |
| `profiles` | Object (map) | No | Credential profiles, keyed by profile ID. |
## Shared defaults
The top-level defined `defaultIssuerKey`, `defaultIssuerDid` and `defaultIssuerX5chain` keys are global configuration values which can be used across multiple profiles via the `${...}` syntax:
```hocon
defaultIssuerKey = {
type = "jwk"
jwk = {
kty = "EC"
d = "..."
crv = "P-256"
x = "..."
y = "..."
}
}
defaultIssuerDid = "did:jwk:eyJrdHk..."
profiles = {
identityCredential = {
name = "IdentityCredential"
credentialConfigurationId = "IdentityCredential_jwt_vc_json"
issuerKey = ${defaultIssuerKey} # <- substitution
issuerDid = ${defaultIssuerDid} # <- substitution
credentialData = { ... }
}
}
```
**Info:**
This pattern is just a convenience for sharing one key across many profiles. A profile may equally declare its own inline `issuerKey` / `issuerDid` instead of referencing the defaults.
## Profile properties
| Property | Type | Required | Description |
|----------|------|----------|-------------|
| `name` | String | **Yes** | Human-readable profile name (non-blank). |
| `credentialConfigurationId` | String | **Yes** | Must match a key in `credentialConfigurations` in `credential-issuer-metadata.conf`. |
| `issuerKey` | Object | **Yes** | Serialized signing key. Must contain a `type` field. |
| `credentialData` | Object | **Yes** | The credential body template (claims). |
| `issuerDid` | String | No | Issuer DID. Used by `jwt_vc_json` and (optionally) `dc+sd-jwt`. |
| `mapping` | Object | No | Dynamic [data functions](https://docs.walt.id/community-stack/issuer2/data-functions.md) for runtime fields. |
| `selectiveDisclosure` | Object | No | SD-JWT selective-disclosure configuration. |
| `idTokenClaimsMapping` | Object | No | Maps authorization-code-flow ID-token claims into the credential. |
| `mDocNameSpacesDataMappingConfig` | Object | No | Per-namespace JSON→CBOR type conversions for `mso_mdoc`. |
| `authorizedTransactionDataTypes` | Array<String> | No | [OID4VP](https://docs.walt.id/concepts/data-exchange-protocols/openid4vp.md) transaction-data types this mDoc's device key may sign. Embedded in the MSO as `KeyAuthorizations`. See [Authorized transaction data types](#authorized-transaction-data-types). |
| `x5Chain` | Array<String> | No | X.509 certificate chain (PEM) for x5c-based signing. |
| `notifications` | Object | No | Issuance webhook configuration. |
| `credentialStatus` | Object | No | Credential status (revocation) configuration. |
**Error:**
`name`, `credentialConfigurationId`, `issuerKey` and `credentialData` are all **required** — a profile that omits any of them stops the service from loading. `issuerKey` must contain a `type` field, and `credentialConfigurationId` must reference a credential configuration that actually exists in the metadata file.
### Where each field sits
Every property in the table above is a **direct child of a single profile** — they all sit at the same level, inside one entry of the `profiles` map. The skeleton below shows that layout (the sections further down zoom into each block):
```hocon
profiles = {
myProfile = { # profile ID (left of the "=")
# ---- required ----
name = "..." # string
credentialConfigurationId = "..." # string, must exist in credential-issuer-metadata.conf
issuerKey = { type = "...", ... } # object
credentialData = { ... } # object (the credential body template)
# ---- optional ----
issuerDid = "did:..." # string
mapping = { ... } # object (data functions)
selectiveDisclosure = { # object (SD-JWT only)
fields = { ... }
decoyMode = "NONE"
decoys = 0
}
idTokenClaimsMapping = { # object: "" = ""
"$.given_name" = "$.given_name"
}
mDocNameSpacesDataMappingConfig = { # object (mso_mdoc only), keyed by namespace
"org.iso.18013.5.1" = { entriesConfigMap = { ... } }
}
authorizedTransactionDataTypes = [ # array of strings (mso_mdoc only)
"urn:eudi:sca:payment:1"
]
x5Chain = [ "..." ] # array of PEM strings
notifications = { # object
webhook = { url = "..." }
}
credentialStatus = { ... } # object
}
}
```
### Profile ID and uniqueness rules
Inside the `profiles` block, each entry's **name on the left of the `=` is its profile ID**. In the example below, `identityCredential` is the profile ID:
```hocon
profiles = {
identityCredential = { # <- "identityCredential" is the profile ID
name = "IdentityCredential"
# ...
}
}
```
- A profile ID must be non-blank and must **not contain a `.` character**.
- Only **one profile per `credentialConfigurationId`** is allowed — two profiles pointing at the same configuration ID will break issuance for that credential.
### Signing: what's needed for each credential type
Which signing fields a profile needs depends on the credential format:
| Format | Issuer mode |
|--------|-------------|
| `jwt_vc_json` (W3C JWT VC) | `issuerKey` + `issuerDid` (no `x5Chain`). |
| `mso_mdoc` (mDoc) | `issuerKey` + `x5Chain` (no `issuerDid`). |
| `dc+sd-jwt` (SD-JWT VC) | `issuerKey` + **exactly one** of `issuerDid` *or* `x5Chain`. |
## Issuer Key
A **serialized walt.id key**. The `type` field selects the backend and is required. The same key backends apply as elsewhere in walt.id:
| `type` | Backend |
|--------|---------|
| `jwk` | Local in-memory / file JWK. |
| `tse` | HashiCorp Vault Transit Engine. |
| `aws-rest-api` | AWS KMS. |
| `azure-rest-api` | Azure Key Vault. |
| `oci-rest-api` | Oracle Cloud KMS. |
**Error:**
The KMS backend identifiers are `aws-rest-api`, `azure-rest-api` and `oci-rest-api` — **not** `aws`, `azure` or `oci`. Using a short form is an unregistered type and fails at startup.
**Option: Local JWK**
The `jwk` field may be written as a nested HOCON object (the form used by the shipped config):
```hocon
issuerKey = {
type = "jwk"
jwk = {
kty = "EC"
d = "..."
crv = "P-256"
x = "..."
y = "..."
}
}
```
**Option: HashiCorp Vault**
```hocon
issuerKey = {
type = "tse"
server = "http://vault:8200/v1/transit"
id = "my-issuer-key"
auth = {
accessKey = "hvs.your-vault-token"
}
# Optional, for Vault Enterprise namespaces:
# namespace = "my-namespace"
}
```
**Option: AWS KMS**
```hocon
issuerKey = {
type = "aws-rest-api"
id = "arn:aws:kms:eu-central-1:123456789012:key/abcd-..."
config = {
auth = {
accessKeyId = "..."
secretAccessKey = "..."
region = "eu-central-1"
}
}
}
```
**Option: Azure Key Vault**
```hocon
issuerKey = {
type = "azure-rest-api"
id = "https://my-vault.vault.azure.net/keys/my-key/"
auth = {
clientId = "..."
clientSecret = "..."
tenantId = "..."
keyVaultUrl = "https://my-vault.vault.azure.net/"
}
}
```
## Credential Data
`credentialData` is the static data template for the credential body. Fields that should be computed per issuance are filled in by [data functions](https://docs.walt.id/community-stack/issuer2/data-functions.md) defined in the `mapping` object such as ``, ``, ``, `` and ``:
```hocon
credentialData = {
"@context" = ["https://www.w3.org/2018/credentials/v1"]
type = ["VerifiableCredential", "IdentityCredential"]
credentialSubject = {
id = "THIS WILL BE REPLACED WITH DYNAMIC DATA FUNCTION"
given_name = "John"
family_name = "Doe"
}
issuer = { id = "THIS WILL BE REPLACED WITH DYNAMIC DATA FUNCTION" }
}
mapping = {
id = ""
issuer = { id = "" }
credentialSubject = { id = "" }
issuanceDate = ""
expirationDate = ""
}
```
For SD-JWT VC, time claims are typically driven with the seconds variants:
```hocon
mapping = {
id = ""
iat = ""
nbf = ""
exp = ""
}
```
## Selective Disclosure
For `dc+sd-jwt` credentials, declare which claims are selectively disclosable. Each field carries `sd` (boolean), and nested objects can be described with `children`. The block may also configure decoy digests:
```hocon
selectiveDisclosure = {
fields = {
birth_date = { sd = true }
family_name = { sd = false }
address = {
sd = true
children = {
fields = {
street = { sd = true }
city = { sd = true }
}
}
}
}
decoyMode = "NONE" # NONE | FIXED | RANDOM
decoys = 0
}
```
## ID Token Claim Mapping
For the authorization-code flow, copy claims from the IdP's ID token into the credential. Each entry is **`"" = ""`**:
- The **key** is a JSONPath into the **ID token** (the source of the value).
- The **value** is a JSONPath into `credentialData` (where the value is written).
```hocon
idTokenClaimsMapping = {
# read $.family_name from the ID token, write it to $.registered_family_name in credentialData
"$.family_name" = "$.registered_family_name"
"$.given_name" = "$.registered_given_name"
}
```
**Error:**
**Both sides must be full JSONPath expressions** (starting with `$`), and **both must already resolve to a non-null value** — the source path must exist in the ID token, and the destination path must already exist in your `credentialData` template. The mapping *updates* an existing field; it does not create new ones. Bare keys like `given_name` or `address.street` are **not** valid here.
## mDocNameSpacesDataMappingConfig
### Why this is needed
mDoc credentials (`mso_mdoc`) are **CBOR-encoded** as defined by [ISO/IEC 18013-5](https://www.iso.org/standard/69084.html), and the standard mandates a specific CBOR type for certain data elements:
- **Dates** such as `birth_date`, `issue_date` and `expiry_date` must be a CBOR `full-date`; timestamps must be a `tdate`.
- **Binary** values such as `portrait` must be a CBOR **byte string** (`bstr`).
But you author `credentialData` as JSON, which has no native date or byte-string type — those values arrive as plain text strings. Without instructions, the issuer encodes them as CBOR **text strings**, which is **not standards-conformant**: the resulting credential does not match the CBOR types that ISO/IEC 18013-5 (and profiles built on it, such as the EUDI PID Rulebook) require for those data elements.
`mDocNameSpacesDataMappingConfig` is what tells the issuer which JSON string fields to re-encode into the correct CBOR type.
### Is it required?
The field itself is **optional** — but it is **effectively required for any data element the standard defines as a non-text type** (dates, binary). Plain text and numeric fields (e.g. `family_name`, `document_number`) need no entry; only list the fields that require a conversion.
### Syntax
`credentialData` is organized by namespace, and each namespace maps to an `entriesConfigMap` keyed by claim name:
```hocon
mDocNameSpacesDataMappingConfig = {
"org.iso.18013.5.1" = {
entriesConfigMap = {
birth_date = { type = "string", conversionType = "stringToFullDate" }
issue_date = { type = "string", conversionType = "stringToFullDate" }
portrait = { type = "string", conversionType = "base64StringToByteString" }
# Arrays and nested objects are described recursively:
driving_privileges = {
type = "array"
arrayConfig = [
{
type = "object"
entriesConfigMap = {
issue_date = { type = "string", conversionType = "stringToFullDate" }
expiry_date = { type = "string", conversionType = "stringToFullDate" }
}
}
]
}
}
}
}
```
**Supported `conversionType` values:**
| Value | Effect |
|-------|--------|
| `stringToFullDate` | Parses a date string into a CBOR full-date element. |
| `stringToTDate` | Parses a date-time string into a CBOR `tdate` element. |
| `base64StringToByteString` | Decodes standard Base64 into a CBOR byte string. |
| `base64UrlStringToByteString` | Decodes Base64URL into a CBOR byte string. |
## Authorized transaction data types
For `mso_mdoc` credentials that must later present OpenID4VP `transaction_data`, the device key can only device-sign types that appear in the MSO's `KeyAuthorizations`. Issuer2 embeds that grant from `authorizedTransactionDataTypes`.
```hocon
authorizedTransactionDataTypes = ["urn:eudi:sca:payment:1"]
```
What the issuer writes into the MSO for each listed type:
- Namespace = the transaction-data type string itself (walt.id convention, matching what the wallet presenter emits)
- Data elements = `transaction_data_hash` and `transaction_data_hash_alg`
- No blanket `nameSpaces` authorization — only those hash elements are granted
Blank entries are dropped, duplicates collapsed. If the list is null or empty after filtering, no `KeyAuthorizations` are emitted.
**Info:**
Presentation of an unlisted type is rejected for that mDoc. Register the same type in [`transaction-data-profiles.conf`](https://docs.walt.id/community-stack/verifier2/configurations/config-files/transaction-data-profiles.md) on verifier and wallet, and list it here on the issuing profile.
**Note:**
This field applies to **mDoc (`mso_mdoc`) only**. SD-JWT VC transaction-data binding uses the KB-JWT and does not consume `authorizedTransactionDataTypes`.
**Example** — demo SCA payment card profile (not a normative EUDI TS-12 SCA Attestation):
```hocon
scaPaymentCardMdoc = {
name = "SCA Payment Card (demo)"
credentialConfigurationId = "sca_payment_card_mso_mdoc"
issuerKey = ${defaultIssuerKey}
credentialData = {
"eu.europa.ec.eudi.sca.payment_card.1" = {
card_scheme = "visa"
card_last4 = "4242"
pan_reference = "V0010013020000000000004301679"
card_holder_name = "Jane Doe"
expiry_date = "2028-12"
}
}
authorizedTransactionDataTypes = ["urn:eudi:sca:payment:1"]
x5Chain = ${defaultIssuerX5chain}
}
```
## Notifications
Configure an issuance webhook, learn more [here](https://docs.walt.id/community-stack/issuer2/notifications.md):
```hocon
notifications = {
webhook = {
url = "https://example.com/webhook/issuance"
}
}
```
## Credential Status
Embed a status-list reference into every credential issued from this profile (e.g. for revocation/suspension). The accepted shape is **format-specific**:
- **W3C JWT VC** (`jwt_vc_json`) — the object is embedded verbatim as the `credentialStatus` claim:
```hocon
credentialStatus = {
type = "BitstringStatusListEntry"
id = "https://issuer.example.com/status/1#94567"
statusPurpose = "revocation"
statusListIndex = "94567"
statusListCredential = "https://issuer.example.com/status/1"
}
```
- **SD-JWT VC** (`dc+sd-jwt`) and **mDoc** (`mso_mdoc`) — a Token Status List reference with `idx` and `uri`:
```hocon
credentialStatus = {
status_list = {
idx = 94567
uri = "https://issuer.example.com/status/1"
}
}
```
**Info:**
In the Community Stack you host and serve the status list yourself — the issuer only embeds this reference into the credential. The same value can also be supplied as a per-offer runtime override. See [Credential Status](https://docs.walt.id/community-stack/issuer2/credential-offers/credential-status.md) for the full reference across all formats.
## Example Configuration
```hocon
# issuer2-profiles.conf
defaultIssuerKey = {
type = "jwk"
jwk = {
kty = "EC"
d = "..."
crv = "P-256"
x = "..."
y = "..."
}
}
defaultIssuerDid = "did:jwk:eyJrdHk..."
defaultIssuerX5chain = [
"""-----BEGIN CERTIFICATE-----
MIIBeTCCAR8CFHrWgrGl5KdefSvRQhR...
-----END CERTIFICATE-----"""
]
profiles = {
# W3C JWT VC — uses issuerDid
identityCredential = {
name = "IdentityCredential"
credentialConfigurationId = "IdentityCredential_jwt_vc_json"
issuerKey = ${defaultIssuerKey}
issuerDid = ${defaultIssuerDid}
credentialData = {
"@context" = ["https://www.w3.org/2018/credentials/v1"]
type = ["VerifiableCredential", "IdentityCredential"]
credentialSubject = {
id = "THIS WILL BE REPLACED WITH DYNAMIC DATA FUNCTION"
given_name = "John"
family_name = "Doe"
}
issuer = { id = "THIS WILL BE REPLACED WITH DYNAMIC DATA FUNCTION" }
}
mapping = {
id = ""
issuer = { id = "" }
credentialSubject = { id = "" }
issuanceDate = ""
expirationDate = ""
}
}
# mDoc — uses x5Chain, no issuerDid
mDL = {
name = "ISO 18013-5 mDL"
credentialConfigurationId = "org.iso.18013.5.1.mDL"
issuerKey = ${defaultIssuerKey}
credentialData = {
"org.iso.18013.5.1" = {
family_name = "Mustermann"
given_name = "Erika"
birth_date = "1971-09-01"
}
}
mDocNameSpacesDataMappingConfig = {
"org.iso.18013.5.1" = {
entriesConfigMap = {
birth_date = { type = "string", conversionType = "stringToFullDate" }
}
}
}
x5Chain = ${defaultIssuerX5chain}
}
}
```
## Related Configuration
- [Credential Issuer Metadata](https://docs.walt.id/community-stack/issuer2/configurations/config-files/credential-issuer-metadata.md) – Declares the credential configurations referenced by `credentialConfigurationId`
- [Issuer Service](https://docs.walt.id/community-stack/issuer2/configurations/config-files/issuer-service.md) – Core service configuration
- [Data Functions](https://docs.walt.id/community-stack/issuer2/data-functions.md) – Dynamic data population in `mapping`
--------------------------------------------------------------------------------
PAGE: Authentication Service
URL: https://docs.walt.id/community-stack/issuer2/configurations/config-files/authentication-service
RAW: https://docs.walt.id/community-stack/issuer2/configurations/config-files/authentication-service.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Authentication Service Configuration
The `authentication-service.conf` file configures the **external OAuth 2.0 / OpenID Connect (OIDC) provider** (such as Keycloak, Microsoft Entra ID or Auth0) used for authorization-code-flow credential issuance. During that flow the issuer sends the user to this provider to log in, and then receives the user's claims (e.g. email, name) in return.
To use authorization code flow you configure your own OAuth/OIDC provider here — its endpoints, the client credentials the issuer uses to talk to it, and the scopes to request.
**Note:**
**Just want to try it out?** You don't need to set anything up. If you don't provide an `authentication-service.conf`, Issuer2 falls back to a hosted walt.id test identity provider with ready-made login accounts — see [Default Test Provider](#default-test-provider) below. Configure your own provider only when you move towards production.
## File Location
```
waltid-services/waltid-issuer-api2/config/authentication-service.conf
```
## When This Is Used
This configuration only matters when issuing credentials via **authorization code flow**. In that flow the issuer sends the user to the provider configured here to log in. The provider returns the user's claims (e.g. email, name) in an ID token, which can then be mapped into the credential.
For **pre-authorized code flow** (PIN-based issuance), this configuration is not used.
## Default Test Provider
To let you try the authorization code flow immediately — without registering your own Keycloak, Entra, or Auth0 tenant — Issuer2 ships with built-in defaults that point at a **hosted walt.id test identity provider** (a Keycloak realm at `keycloak.demo.walt.id`). These defaults are used automatically whenever a value isn't set in `authentication-service.conf`, so if you never create that file, the authorization code flow works out of the box against this test provider.
You can then create an `AUTHORIZED` offer (see [Create an Offer](https://docs.walt.id/community-stack/issuer2/credential-offers/create-offer.md#authorization-code-flow)) and, when the wallet sends you to log in, sign in with one of the accounts below.
### Test Users
| Username | Password | Claims returned in the ID token |
|----------|----------|---------------------------------|
| `jane@walt.id` | `jane` | `given_name`, `family_name` |
| `max@walt.id` | `password` | `clearanceLevel`, `employeeId`, `position`, `department`, `startDate` |
These claims are what the provider returns after login. To actually see them land in an issued credential, the credential profile needs an [`idTokenClaimsMapping`](https://docs.walt.id/community-stack/issuer2/configurations/config-files/issuer-profiles.md#id-token-claim-mapping) that maps them into credential fields.
**Try it end to end with `jane`:** the ready-made `identityCredentialSdJwt` profile already maps `given_name` and `family_name` from the ID token. Create an `AUTHORIZED` offer for that profile, open it in a wallet, log in as `jane@walt.id` / `jane`, and the issued credential's `given_name` / `family_name` will be filled in from Jane's login instead of the profile's placeholder values.
`max@walt.id` returns a richer set of workplace claims (`clearanceLevel`, `employeeId`, `position`, `department`, `startDate`). No default profile maps these, so use `max` when you want to practise writing your own `idTokenClaimsMapping` against real ID-token claims.
**Error:**
The default test provider and these shared accounts are for local testing and demos only. They are public and unauthenticated for anyone running Issuer2, so never rely on them — or the built-in client secret — in production. Configure your own provider (see [Example Configurations](#example-configurations)) before going live.
## Configuration Options
Every option has a built-in default that points at the [hosted test provider](#default-test-provider). You only need to set these when pointing Issuer2 at **your own** provider — at which point `authorizeUrl`, `accessTokenUrl`, `clientId`, and `clientSecret` are effectively required (the test-provider defaults won't match your setup).
| Property | Type | Required for own provider | Default | Description |
|----------|------|----------|---------|-------------|
| `authorizeUrl` | String | **Yes** | Test provider | OAuth authorization endpoint. The user is sent here to authenticate. |
| `accessTokenUrl` | String | **Yes** | Test provider | Token endpoint where the issuer exchanges the authorization code for an access token and ID token. |
| `clientId` | String | **Yes** | Test provider | OAuth client ID registered with the provider. |
| `clientSecret` | String | **Yes** | Test provider | OAuth client secret. |
| `name` | String | No | `"keycloak"` | Provider name. Informational — passed through to the OAuth client settings. |
| `defaultScopes` | Array<String> | No | `["openid", "profile"]` | OAuth scopes requested during authorization. |
| `forwardIssuerStateToAuthorizationServer` | Boolean | No | `false` | Whether to forward the issuer's internal `issuer_state` to the authorization server. |
### name
Provider name. Informational only — it is passed through as the OAuth client's `name` and does not change behaviour.
```hocon
name = "keycloak"
```
### authorizeUrl
OAuth authorization endpoint URL. The user is redirected here to authenticate.
```hocon
authorizeUrl = "https://keycloak.example.com/realms/myrealm/protocol/openid-connect/auth"
```
### accessTokenUrl
Token endpoint URL where the issuer exchanges the authorization code for an access token and ID token.
```hocon
accessTokenUrl = "https://keycloak.example.com/realms/myrealm/protocol/openid-connect/token"
```
### clientId
OAuth client ID registered with the provider.
```hocon
clientId = "issuer_api"
```
### clientSecret
OAuth client secret.
```hocon
clientSecret = "secret-from-your-provider"
```
**Error:**
Never commit secrets to version control. Use environment variables or a secret manager.
### defaultScopes
OAuth scopes requested during authorization. Common scopes:
- `openid` – required for OIDC
- `profile` – access to name and similar profile claims
- `email` – access to the email address
```hocon
defaultScopes = ["openid", "profile"]
```
### forwardIssuerStateToAuthorizationServer
Controls whether the issuer's internal `issuer_state` value is forwarded to the **authorization server** as an extra `issuer_state` query parameter on the authorize request. Defaults to `false`.
This is only needed for custom integrations where the external authorization server has to receive the issuer state. When `true`, the issuer extracts `issuer_state` from the internal login request and appends it to the outgoing authorize URL (it is skipped if the value is missing, blank, or already present).
```hocon
forwardIssuerStateToAuthorizationServer = false
```
Leave this `false` unless your provider specifically requires the forwarded state.
## Redirect URI
The issuer's OAuth callback is derived from `baseUrl` (set in [`issuer-service.conf`](https://docs.walt.id/community-stack/issuer2/configurations/config-files/issuer-service.md)):
```
/openid4vci/external/oauth/callback
```
Register this exact URL as an allowed redirect URI in your OAuth provider, otherwise the provider rejects the login. For example, with `baseUrl = "https://issuer.example.com"` the redirect URI is `https://issuer.example.com/openid4vci/external/oauth/callback`.
## Example Configurations
### Keycloak
```hocon
# authentication-service.conf
name = "keycloak"
authorizeUrl = "https://keycloak.example.com/realms/myrealm/protocol/openid-connect/auth"
accessTokenUrl = "https://keycloak.example.com/realms/myrealm/protocol/openid-connect/token"
clientId = "issuer_api"
clientSecret = "secret-from-keycloak"
defaultScopes = ["openid", "profile", "email"]
forwardIssuerStateToAuthorizationServer = false
```
### Microsoft Entra ID (Azure AD)
```hocon
# authentication-service.conf
name = "entra"
authorizeUrl = "https://login.microsoftonline.com/TENANT_ID/oauth2/v2.0/authorize"
accessTokenUrl = "https://login.microsoftonline.com/TENANT_ID/oauth2/v2.0/token"
clientId = "client-id-from-azure"
clientSecret = "client-secret-from-azure"
defaultScopes = ["openid", "profile", "email"]
forwardIssuerStateToAuthorizationServer = false
```
### Auth0
```hocon
# authentication-service.conf
name = "auth0"
authorizeUrl = "https://YOUR_DOMAIN.auth0.com/authorize"
accessTokenUrl = "https://YOUR_DOMAIN.auth0.com/oauth/token"
clientId = "client-id-from-auth0"
clientSecret = "client-secret-from-auth0"
defaultScopes = ["openid", "profile", "email"]
forwardIssuerStateToAuthorizationServer = false
```
## Mapping ID Token Claims to Credentials
After successful authorization, the issuer receives an ID token with user claims. Use `idTokenClaimsMapping` in your credential profile to map those claims into credential fields.
See [Issuer Profiles – ID Token Claim Mapping](https://docs.walt.id/community-stack/issuer2/configurations/config-files/issuer-profiles.md#id-token-claim-mapping).
## Related Configuration
- [Issuer Service](https://docs.walt.id/community-stack/issuer2/configurations/config-files/issuer-service.md) – Sets the `baseUrl` the redirect URI is built from.
- [Issuer Profiles](https://docs.walt.id/community-stack/issuer2/configurations/config-files/issuer-profiles.md) – Maps ID token claims to credential fields.
- [Credential Offers](https://docs.walt.id/community-stack/issuer2/credential-offers/create-offer.md) – Create authorization-code-flow offers.
--------------------------------------------------------------------------------
PAGE: Dev Mode
URL: https://docs.walt.id/community-stack/issuer2/configurations/config-files/dev-mode
RAW: https://docs.walt.id/community-stack/issuer2/configurations/config-files/dev-mode.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Dev Mode Configuration
The `dev-mode.conf` file holds development-only settings. In the issuer it currently exposes a single option: whether `did:web` resolution may use plain HTTP instead of HTTPS only.
## File Location
```
waltid-services/waltid-issuer-api2/config/dev-mode.conf
```
## Enabling It
`dev-mode.conf` is only read when the `dev-mode` feature is enabled in `_features.conf`:
```hocon
# _features.conf
enabledFeatures = ["dev-mode"]
```
```hocon
# dev-mode.conf
enableDidWebResolverHttps = false
```
If the feature is enabled but `dev-mode.conf` can't be loaded, the issuer logs a warning and keeps the secure default.
## `enableDidWebResolverHttps`
| Value | Effect |
|-------|--------|
| `true` (default) | `did:web` documents are fetched over **HTTPS** only |
| `false` | `did:web` documents may be fetched over **HTTP** |
`did:web` resolves a DID by fetching a document over the network — e.g. `did:web:localhost:7005` → `http://localhost:7005/.well-known/did.json`. Local services usually run without TLS, so HTTP resolution must be allowed to use `did:web:localhost`. This is the only behaviour the `dev-mode` feature affects in the issuer; enabling the feature without setting this to `false` changes nothing.
## Related Configuration
- [Features](https://docs.walt.id/community-stack/issuer2/configurations/config-files/features.md) – Enable/disable features
- [Issuer Service](https://docs.walt.id/community-stack/issuer2/configurations/config-files/issuer-service.md) – Configure issuer DID
--------------------------------------------------------------------------------
PAGE: Persistence
URL: https://docs.walt.id/community-stack/issuer2/configurations/config-files/persistence
RAW: https://docs.walt.id/community-stack/issuer2/configurations/config-files/persistence.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Persistence Configuration
The `persistence.conf` file chooses where the issuer keeps short-lived [OID4VCI](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md) state (issuance sessions, codes, tokens). The default is in-memory storage; switch to Redis when you run more than one issuer instance and they need to share that state.
## File Location
```
waltid-services/waltid-issuer-api2/config/persistence.conf
```
## When This Is Used
`persistence.conf` is only read when the `persistence` feature is enabled in `_features.conf`:
```hocon
# _features.conf
enabledFeatures = ["persistence"]
```
Without the feature, the issuer always uses in-memory storage, so all state is lost on restart and cannot be shared across instances.
## Configuration Options
| Property | Type | Required | Default | Description |
|----------|------|----------|---------|-------------|
| `type` | String | No | `"memory"` | `memory`, `redis`, or `redis-cluster`. |
| `nodes` | Array<{host, port}> | For Redis | – | Redis nodes. `redis` requires **exactly one** node; `redis-cluster` takes one or more. |
| `user` | String | No | – | Redis username (Redis ACL / AUTH). |
| `password` | String | No | – | Redis password. |
A `redis` type with zero or multiple nodes fails at startup ("Redis persistence requires defining at least 1 node!" / "exactly 1 node is required").
**Error:**
Don't commit `password` to version control — supply it via an environment variable or secret manager.
## What Is Stored
When persistence is enabled, these stores move from memory into Redis:
| Store | Holds | Fallback TTL |
|-------|-------|--------------|
| Issuance sessions | Credential offer state and subject data | 5 min |
| Authorization codes | Authorization-code-flow codes | 5 min |
| Pre-authorized codes | Pre-authorized-code-flow codes and tx codes | 5 min |
| Pushed authorization requests (PAR) | PAR request state | 90 s |
| Refresh tokens | OpenID4VCI refresh tokens | 1 day |
Each entry is actually stored with a TTL until its own `expiresAt`; the values above are only the fallback used when no explicit expiry is set. Access tokens are self-contained and are **not** stored here.
## Example Configurations
```hocon
# persistence.conf — in-memory (default)
type = "memory"
```
```hocon
# persistence.conf — single Redis
type = "redis"
nodes = [{ host = "redis", port = 6379 }]
# user = "issuer-service"
# password = "secret-from-secrets-manager"
```
```hocon
# persistence.conf — Redis cluster
type = "redis-cluster"
nodes = [
{ host = "redis-node-1", port = 6379 },
{ host = "redis-node-2", port = 6379 },
{ host = "redis-node-3", port = 6379 }
]
```
## Docker Compose
Connection details come from `nodes` in `persistence.conf` (mounted into the container), not from environment variables. Point `nodes` at the Redis service name:
```yaml
services:
issuer-api2:
image: waltid/issuer-api2:latest
ports:
- "7005:7005"
volumes:
- ./config:/waltid-issuer-api2/config
depends_on:
- redis
redis:
image: redis:7-alpine
command: redis-server --appendonly yes
volumes:
- redis-data:/data
volumes:
redis-data:
```
## Related Configuration
- [Features](https://docs.walt.id/community-stack/issuer2/configurations/config-files/features.md) – Enable the persistence feature
- [Web](https://docs.walt.id/community-stack/issuer2/configurations/config-files/web.md) – Web server configuration
--------------------------------------------------------------------------------
PAGE: Web
URL: https://docs.walt.id/community-stack/issuer2/configurations/config-files/web
RAW: https://docs.walt.id/community-stack/issuer2/configurations/config-files/web.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Web Server Configuration
The `web.conf` file controls the HTTP server: which address and port it binds to, and how JSON responses are encoded. It is always read — there is no feature flag to enable.
## File Location
```
waltid-services/waltid-issuer-api2/config/web.conf
```
## Configuration Options
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `webHost` | String | `"0.0.0.0"` | Network interface to bind to. `0.0.0.0` = all interfaces; `127.0.0.1` = localhost only. |
| `webPort` | Int | `3000` | TCP port to listen on. The issuer ships a `web.conf` that sets this to `7005`. |
| `humanReadableResultEncoding` | Boolean | `false` | When `true`, JSON responses are pretty-printed. Useful for debugging; leave `false` in production. |
**Info:**
`3000` is the built-in default in code. The `web.conf` bundled with the issuer overrides it to `7005`, so a standalone issuer listens on `7005` unless you change it.
## Example
```hocon
# web.conf
webHost = "0.0.0.0"
webPort = 7005
humanReadableResultEncoding = false
```
For local development you can restrict access to your machine and pretty-print responses:
```hocon
# web.conf
webHost = "127.0.0.1"
webPort = 7005
humanReadableResultEncoding = true
```
## Docker
For the container to be reachable through a published port, the server must bind to all interfaces (`webHost = "0.0.0.0"`, the default) and you map the port on the host:
```bash
docker run -p 7005:7005 \
-v $(pwd)/config:/waltid-issuer-api2/config \
waltid/issuer-api2:latest
```
In the bundled Docker Compose stack each service gets its own host port to avoid collisions. Issuer2 is published on `7005` so it doesn't clash with the v1 issuer on `7002`:
| Service | Compose port |
|---------|--------------|
| Wallet API | 7001 |
| Issuer API (v1) | 7002 |
| Verifier API (v1) | 7003 |
| Verifier API2 | 7004 |
| Issuer API2 | 7005 |
| Wallet API2 | 7006 |
These ports come from `.env` / `docker-compose.yaml`, not from `web.conf`.
## Health Check
When the `healthchecks` feature is enabled (it is by default), the server exposes a liveness endpoint that returns HTTP `200` once it is ready:
```bash
curl -i http://localhost:7005/livez
```
## Related Configuration
- [Issuer Service](https://docs.walt.id/community-stack/issuer2/configurations/config-files/issuer-service.md) – Set `baseUrl` to match your public endpoint
- [Features](https://docs.walt.id/community-stack/issuer2/configurations/config-files/features.md) – Enable/disable API features
--------------------------------------------------------------------------------
PAGE: Data Functions
URL: https://docs.walt.id/community-stack/issuer2/data-functions
RAW: https://docs.walt.id/community-stack/issuer2/data-functions.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Data Functions
The Issuer2 API offers the use of **data functions** when issuing credentials. These functions ensure that credentials always contain the most current and accurate information.
**Note:**
Data functions are supported across all credential types: **W3C JWT VC**, **IETF SD-JWT VC**, and **ISO mDoc**.
## How Data Functions Work
Data functions are not executed at the moment you create a credential offer. Instead, they get executed at the point when the credential is actually claimed by the recipient. This means that the data within the credential stays up-to-date, regardless of whether the recipient claims it immediately or after a considerable period.
## The Importance of Timing
This delayed execution is crucial because it guarantees the information's relevance and accuracy. For example, a credential might be issued today but claimed a month later. The data functions ensure that the credential reflects any changes that occurred during that interval, maintaining the credibility of the credential.
## How to Use Data Functions
When configuring a credential profile or creating an offer, you can provide a `mapping` object that defines which keys in the credential data structure should be replaced by the value returned from the provided data function.
### Example
**Credential Data Template**
```json
{
"@context": [
"https://www.w3.org/ns/credentials/v2",
"https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json"
],
"id": "",
"type": ["VerifiableCredential", "OpenBadgeCredential"],
"issuer": {
"id": ""
},
"credentialSubject": {
"achievement": ""
}
}
```
**Mapping Object with Data Functions**
```json
{
"id": "",
"issuer": {
"id": ""
},
"credentialSubject": {
"id": "",
"achievement": ""
},
"issuanceDate": "",
"expirationDate": ""
}
```
**Issued Credential**
```json
{
"@context": [
"https://www.w3.org/ns/credentials/v2",
"https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json"
],
"id": "urn:uuid:3732a5b8-...",
"type": ["VerifiableCredential", "OpenBadgeCredential"],
"issuer": {
"id": "did:key:z6Mk..."
},
"credentialSubject": {
"id": "did:key:z6Mk...",
"achievement": {
"type": ["Achievement"],
"name": "Teamwork"
}
},
"issuanceDate": "2024-01-15T10:30:00Z",
"expirationDate": "2025-01-15T10:30:00Z"
}
```
## Available Functions
### UUID
`` generates a unique identifier.
```json
{
"id": ""
}
```
**Result:** `"urn:uuid:3732a5b8-1234-5678-9abc-def012345678"`
### Subject DID
`` returns the recipient's DID once the credential is claimed.
```json
{
"credentialSubject": {
"id": ""
}
}
```
### Issuer DID
`` returns the issuer's DID.
```json
{
"issuer": {
"id": ""
}
}
```
### Timestamp
`` returns the current timestamp in ISO 8601 format `YYYY-MM-DDThh:mm:ss.sssZ`.
```json
{
"issuanceDate": ""
}
```
**Result:** `"2024-01-15T10:30:00.000Z"`
### Timestamp Seconds
`` returns the Unix timestamp (seconds since epoch).
```json
{
"iat": ""
}
```
**Result:** `1705315800`
### Timestamp In
`` returns a future date by providing the duration as an argument.
```json
{
"expirationDate": ""
}
```
Supported duration formats:
- `30d` - 30 days
- `12h` - 12 hours
- `30m` - 30 minutes
- `1y` - 1 year
**Result:** `"2025-01-15T10:30:00.000Z"`
### Timestamp In Seconds
`` returns a future Unix timestamp.
```json
{
"exp": ""
}
```
**Result:** `1736851800`
### Timestamp Before
`` returns a date in the past.
```json
{
"validFrom": ""
}
```
**Result:** `"2023-12-16T10:30:00.000Z"`
### Timestamp Before Seconds
`` returns a past Unix timestamp.
```json
{
"nbf": ""
}
```
### Display
`` returns the display metadata of the credential as defined in the issuer metadata.
```json
{
"display": ""
}
```
**Result:**
```json
{
"name": "Open Badge Credential",
"locale": "en-US",
"logo": {
"url": "https://w3c-ccg.github.io/vc-ed/plugfest-1-2022/images/JFF_LogoLockup.png",
"alt_text": "JFF Logo"
},
"background_color": "#12107c",
"text_color": "#FFFFFF"
}
```
### Webhook
`` makes a request to a specific URL and inserts the response as plain text.
```json
{
"credentialSubject": {
"studentId": ""
}
}
```
If the webhook returns `"STU-12345"`, the result is:
```json
{
"credentialSubject": {
"studentId": "STU-12345"
}
}
```
### Webhook JSON
`` makes a request to a specific URL and inserts the response as a nested JSON object.
```json
{
"credentialSubject": {
"achievement": ""
}
}
```
If the webhook returns:
```json
{
"type": ["Achievement"],
"name": "Teamwork"
}
```
The result is:
```json
{
"credentialSubject": {
"achievement": {
"type": ["Achievement"],
"name": "Teamwork"
}
}
}
```
## EBSI-Specific Functions
The [EBSI](https://ec.europa.eu/digital-building-blocks/sites/display/EBSI/Home) trust framework mandates a specific timestamp format for issued credentials. Use these functions for EBSI-compliant credentials:
### EBSI Timestamp
`` returns the current timestamp in EBSI format `YYYY-MM-DDThh:mm:ssZ`.
```json
{
"issuanceDate": ""
}
```
**Result:** `"2024-01-15T10:30:00Z"`
### EBSI Timestamp In
`` returns a future date in EBSI format.
```json
{
"expirationDate": ""
}
```
**Result:** `"2025-01-15T10:30:00Z"`
## Using Data Functions in Profiles
Configure data functions in your credential profile:
```hocon
# issuer2-profiles.conf
profiles = {
openBadgeCredential = {
name = "OpenBadgeCredential"
credentialConfigurationId = "OpenBadgeCredential_jwt_vc_json"
issuerKey = { ... }
issuerDid = "did:key:z6Mk..."
credentialData = {
"@context" = [
"https://www.w3.org/ns/credentials/v2",
"https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json"
]
type = ["VerifiableCredential", "OpenBadgeCredential"]
credentialSubject = {
type = ["AchievementSubject"]
achievement = {
type = ["Achievement"]
name = ""
}
}
}
mapping = {
id = ""
"issuer.id" = ""
"credentialSubject.id" = ""
"credentialSubject.achievement.name" = ""
issuanceDate = ""
expirationDate = ""
}
}
}
```
## Using Data Functions in Runtime Overrides
Override data functions when creating an offer:
```bash
curl -X POST 'http://localhost:7005/issuer2/credential-offers' \
-H 'Content-Type: application/json' \
-d '{
"profileId": "openBadgeCredential",
"authMethod": "PRE_AUTHORIZED",
"runtimeOverrides": {
"mapping": {
"credentialSubject.achievement.name": ""
}
}
}'
```
## Next Steps
- [Credential Profiles](https://docs.walt.id/community-stack/issuer2/credential-profiles/overview.md) – Configure profiles with data functions
- [Create an Offer](https://docs.walt.id/community-stack/issuer2/credential-offers/create-offer.md) – Create offers with runtime overrides
--------------------------------------------------------------------------------
PAGE: Notifications
URL: https://docs.walt.id/community-stack/issuer2/notifications
RAW: https://docs.walt.id/community-stack/issuer2/notifications.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Notifications
The Issuer2 API supports webhook-based notifications for real-time updates on credential issuance events. Configure notifications at the profile level or override them per offer.
## Overview
Notifications allow your backend systems to receive real-time updates when:
- A credential offer is created or retrieved by a wallet
- Authorization, token, nonce, or credential stages succeed or fail
- A credential is issued
- The issuance session status changes
This enables you to:
- Track issuance progress
- Update your database when credentials are claimed
- Trigger downstream workflows
- Audit credential issuance
## Configuration
Notifications are configured in the `notifications` object of a credential profile or as a runtime override when creating an offer.
### Basic Configuration
```json
{
"notifications": {
"webhook": {
"url": "https://your-server.com/webhook/issuance"
}
}
}
```
## Configuration Properties
| Property | Type | Required | Description |
|----------|------|----------|-------------|
| `webhook.url` | String | Yes | The URL to receive webhook notifications |
## Webhook Events
Your webhook endpoint will receive POST requests with JSON payloads for the following events. The same envelope is used for [SSE](#alternative-server-sent-events-sse).
### Event Structure
```json
{
"target": "session-id-123",
"event": "event_type",
"session": { ... },
"requestId": "3f1c2a8e-9b44-4c1d-8e2f-1a2b3c4d5e6f",
"error": "invalid_grant",
"error_description": "tx_code is invalid"
}
```
| Field | Description |
|-------|-------------|
| `target` | The session/offer ID when the request is correlated; otherwise the `requestId` |
| `event` | The event type |
| `session` | Session data when the request is correlated; `{}` when it is not |
| `requestId` | Call ID for this HTTP request (`X-Request-ID`, generated if omitted) |
| `error` | OAuth / OpenID4VCI error code on failure events |
| `error_description` | Error description returned to the wallet on failure events |
### Event Types
| Stage | Events |
|------|--------|
| Offer | `credential_offer_created`, `credential_offer_retrieved` |
| Pushed authorization | `pushed_authorization_request_succeeded`, `pushed_authorization_request_failed` |
| Authorization | `authorization_request_succeeded`, `authorization_request_failed` |
| Token | `token_request_authorization_code_succeeded`, `token_request_authorization_code_failed`, `token_request_pre_authorized_code_succeeded`, `token_request_pre_authorized_code_failed`, `token_request_refresh_token_succeeded`, `token_request_refresh_token_failed`, `token_request_failed` |
| Nonce | `nonce_request_succeeded`, `nonce_request_failed` |
| Credential | `credential_request_sd_jwt_vc_succeeded`, `credential_request_sd_jwt_vc_failed`, `credential_request_w3c_vc_succeeded`, `credential_request_w3c_vc_failed`, `credential_request_mso_mdoc_succeeded`, `credential_request_mso_mdoc_failed`, `credential_request_failed` |
| Session lifecycle | `issuance_status_changed` |
Webhook and SSE payloads use the lowercase event strings above.
Each protocol endpoint publishes one outcome event. `token_request_failed` and `credential_request_failed` are used when the grant or credential format cannot be resolved. Events include the session only after trusted correlation. Uncorrelated failures and nonce events are published only on `GET /issuer2/events`.
`credential_offer_created` is effectively webhook-only for session subscribers: it is published before the session id is returned, so no session SSE subscriber can exist yet.
### Failure Detail
Failure events carry `error` and `error_description` on the envelope, not a nested `session.failure`:
```json
{
"target": "session-id-123",
"event": "token_request_pre_authorized_code_failed",
"error": "invalid_grant",
"error_description": "tx_code is invalid",
"session": { "sessionId": "session-id-123" }
}
```
For terminal credential failures the same fields are persisted on the session and returned from `GET /issuer2/sessions/{sessionId}`. Retryable `invalid_proof` and `invalid_nonce` failures leave the session active and publish the error on the event only.
Only terminal credential endpoint failures conclude a session — earlier stages leave the grant usable so the wallet can retry.
On successful issuance the session is persisted as `SUCCESSFUL` before the credential success event and `issuance_status_changed` are published. A failure event and `issuance_status_changed` are always separate.
**Note:**
Published session payloads redact issuer signing key material (`issuerKey.type = "redacted"`). Credential data may still be present; use trusted webhook receivers.
### Example Payload
```json
{
"target": "session-id-123",
"event": "credential_offer_retrieved",
"requestId": "3f1c2a8e-9b44-4c1d-8e2f-1a2b3c4d5e6f",
"session": {
"sessionId": "session-id-123",
"credentialConfigurationId": "UniversityDegree_jwt_vc_json",
"credentialOffer": {
"credential_issuer": "http://localhost:7005",
"credential_configuration_ids": ["UniversityDegree_jwt_vc_json"],
"grants": { ... }
}
}
}
```
## Profile-Level Notifications
Configure notifications in your credential profile configuration file:
```hocon
# issuer2-profiles.conf
profiles = {
"notified-credential" = {
name = "Notified Credential"
credentialConfigurationId = "identity_credential_dc+sd-jwt"
issuerKey = { ... }
credentialData = { ... }
notifications = {
webhook = {
url = "https://your-server.com/webhook/issuance"
}
}
}
}
```
## Per-Offer Notifications
Override profile notifications for a specific offer using runtime overrides:
```bash
curl -X POST 'http://localhost:7005/issuer2/credential-offers' \
-H 'Content-Type: application/json' \
-d '{
"profileId": "university-degree",
"authMethod": "PRE_AUTHORIZED",
"runtimeOverrides": {
"notifications": {
"webhook": {
"url": "https://different-server.com/webhook/special-offer"
}
}
}
}'
```
## Alternative: Server-Sent Events (SSE)
For real-time monitoring in browser-based applications, the Issuer2 API also provides a Server-Sent Events (SSE) endpoint. This allows you to subscribe to session events directly from a browser without setting up a webhook server.
**Endpoint:** `GET /issuer2/sessions/{sessionId}/events`
```javascript
const sessionId = 'abc123-def456-ghi789';
const eventSource = new EventSource(
`http://localhost:7005/issuer2/sessions/${sessionId}/events`
);
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('Session event:', data.event);
if (data.event === 'issuance_status_changed' && data.session.status === 'SUCCESSFUL') {
console.log('Credential issued successfully!');
eventSource.close();
}
};
```
The session stream emits the same correlated event types and payload structure as webhooks. Uncorrelated failures and nonce events are available on `GET /issuer2/events`.
**Note:**
On connection, the SSE endpoint first sends an empty `data: {}` message as an initial handshake, before any session events. Guard against it in your handler (e.g. check `data.event` before acting), as shown in the example above where the `data.event === 'issuance_status_changed'` check safely ignores it.
| Feature | Webhooks | SSE |
|---------|----------|-----|
| Best for | Server-to-server | Browser/client apps |
| Connection | Push to your server | Client pulls from issuer |
| Reliability | More reliable | May disconnect |
| Setup | Requires public endpoint | No server needed |
## Next Steps
- [Issuer Profiles Configuration](https://docs.walt.id/community-stack/issuer2/configurations/config-files/issuer-profiles.md) – Add notifications to your profiles
--------------------------------------------------------------------------------
PAGE: Full API Reference
URL: https://docs.walt.id/community-stack/issuer2/full-api-reference
RAW: https://docs.walt.id/community-stack/issuer2/full-api-reference.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Full API Reference
Find the full API reference [here](https://issuer2.demo.walt.id/swagger/index.html)
--------------------------------------------------------------------------------
PAGE: Overview
URL: https://docs.walt.id/community-stack/issuer2/apps/portal2/overview
RAW: https://docs.walt.id/community-stack/issuer2/apps/portal2/overview.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Overview
**Portal2** is a white-label web app for credential issuance and verification using the walt.id
**Issuer2** and **Verifier2** APIs.
## Features
- **Issue credentials** — Issue a credential from a custom JSON payload or a pre-configured [Issuer2 profile](https://docs.walt.id/community-stack/issuer2/credential-profiles/overview.md) to a wallet via [OID4VCI](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md) v1.
- **Verify credentials** — request a credential presentation from a wallet via [OID4VP](https://docs.walt.id/concepts/data-exchange-protocols/openid4vp.md) v1.
- **Simple and Advanced modes** — a guided form for common credential types, or a raw JSON editor for full control over the request payload. Simple mode is the default.
- **Load examples** — payloads can be auto-populated from the Issuer2 and Verifier2 OpenAPI/Swagger specs.
- **Real-time event log** — issuance and verification steps (e.g. `OFFER_RECEIVED`, `CREDENTIAL_ISSUED`) stream in via Server-Sent Events as the wallet interacts with the offer or request.
## Try It Out
- **Live demo:** [portal2.demo.walt.id](https://portal2.demo.walt.id)
- **Local:** included in the [Quick Start](https://docs.walt.id/community-stack/home.md#quick-start) Docker Compose — once running, open [localhost:7105](http://localhost:7105).
## Setup
1. Clone the identity repo
```bash
git clone https://github.com/walt-id/waltid-identity.git && cd waltid-identity
```
2. Running the project
**Option: Local**
1. Change directory
```bash
cd waltid-applications/waltid-web-portal2/
```
2. Configure the dependency services
Create a `.env` file:
```text
NUXT_PUBLIC_ISSUER_BASE=http://localhost:7005
NUXT_PUBLIC_VERIFIER_BASE=http://localhost:7004
NUXT_PUBLIC_WALLET_URL=http://localhost:7101
```
**Note:**
`NUXT_PUBLIC_WALLET_URL` points at the demo [Web Wallet](https://docs.walt.id/community-stack/wallet/apps/overview.md) — there is no Wallet API v2 app yet. Three optional variables (`NUXT_PUBLIC_VERIFIER_KEY_JWK`, `NUXT_PUBLIC_VERIFIER_X5C`, `NUXT_PUBLIC_VERIFIER_CLIENT_ID`) pre-populate the advanced verification security section; leave them empty unless you need them.
3. Run the development server
```bash
npm install
npm run dev
```
The app is available at [localhost:3000](http://localhost:3000).
**Option: Docker**
1. Build & run the image
```bash
docker build -t waltid/portal2 -f waltid-applications/waltid-web-portal2/Dockerfile .
docker run -p 3000:3000 \
-e NUXT_PUBLIC_ISSUER_BASE="http://localhost:7005" \
-e NUXT_PUBLIC_VERIFIER_BASE="http://localhost:7004" \
-e NUXT_PUBLIC_WALLET_URL="http://localhost:7101" \
-i -t waltid/portal2
```
2. Access the portal under [localhost:3000](http://localhost:3000/)
--------------------------------------------------------------------------------
PAGE: Getting Started
URL: https://docs.walt.id/community-stack/wallet2/getting-started
RAW: https://docs.walt.id/community-stack/wallet2/getting-started.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Getting Started
**Wallet API v2** provides the capabilities to build end-to-end digital ID wallets. It manages wallets, keys, DIDs, and credentials, and implements the latest [OID4VCI 1.0](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md) (receiving credentials from issuers) and [OID4VP 1.0](https://docs.walt.id/concepts/data-exchange-protocols/openid4vp.md) (presenting credentials to verifiers) specifications, using [DCQL](https://docs.walt.id/concepts/data-exchange-protocols/dcql.md) for credential matching. Supported formats include [SD-JWT VC (IETF)](https://docs.walt.id/concepts/digital-credentials/sd-jwt-vc.md), [W3C VC](https://docs.walt.id/concepts/digital-credentials/verifiable-credentials-w3c.md), and [ISO 18013-5 mDL](https://docs.walt.id/concepts/digital-credentials/mdoc-mdl-iso.md).
**Note:**
The original wallet service is deprecated. It remains available for existing projects. For new projects, we recommend **Wallet API v2** with its OID4VCI and OID4VP 1.0 support. Migrating from v1? See the [Migration guide](https://docs.walt.id/community-stack/wallet2/migration.md).
## Supported Standards
| | |
|:-------------------------|:----------------------------------------------------------------------------------------------------|
| **Credential Formats:** | [SD-JWT VC (IETF)](https://docs.walt.id/concepts/digital-credentials/sd-jwt-vc.md), [W3C VC (v1.1+, v2.0)](https://docs.walt.id/concepts/digital-credentials/verifiable-credentials-w3c.md), [ISO 18013-5 mDL](https://docs.walt.id/concepts/digital-credentials/mdoc-mdl-iso.md) |
| **Credential Exchange:** | OID4VCI 1.0, OID4VP 1.0 (DCQL) |
| **Signing Algorithms:** | Ed25519, secp256k1, secp256r1, RSA |
## How It Differs from Wallet API v1
| Feature | Wallet API v1 | Wallet API v2 |
|---------|---------------|---------------|
| **OID4VP query language** | Presentation Exchange | DCQL |
| **OID4VCI/OID4VP version** | Draft 11–20 | 1.0 |
| **Persistence** | Built-in | Optional, pluggable |
| **Authentication** | Built-in | Optional, pluggable |
## Setup
If you are new to the stack, the **Docker Compose quick start** is the fastest way to get running — it spins up the Issuer, Verifier, and Wallet together in one command, which lets you issue a credential, receive it into the wallet, and present it, end to end. Once running, the Wallet API v2 is available at `http://localhost:7006`.
**Note:**
Wallet API v2 listens on port `7006` by default, whether run standalone or via Docker Compose (Issuer2 on `7005`, Verifier2 on `7004`).
- [Docker Compose quick start](https://docs.walt.id/community-stack/home.md#quick-start) — Spin up the wallet together with the issuer and verifier. _(Recommended for first-time users)_
- [Run standalone](https://docs.walt.id/community-stack/wallet2/setup.md) — Pull and run the wallet container in isolation using the [walt.id identity repo](https://github.com/walt-id/waltid-identity).
- [Full API reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md) — Explore all endpoints via Swagger.
## Core Concepts
- **Wallets** — A wallet is a container of keys, credentials and DIDs. Learn more on [Managing Wallets](https://docs.walt.id/community-stack/wallet2/wallets.md).
- **Receiving Credentials** — Claim credentials from an issuer over [OID4VCI 1.0](https://docs.walt.id/community-stack/wallet2/credential-receiving/overview.md), via a one-call full flow or step-by-step [Isolated Flow](https://docs.walt.id/community-stack/wallet2/credential-receiving/isolated-flow.md).
- **Presenting Credentials** — Share credentials with a verifier over [OID4VP 1.0](https://docs.walt.id/community-stack/wallet2/credential-presenting/overview.md) via a one-call full flow or step-by-step [Isolated Flow](https://docs.walt.id/community-stack/wallet2/credential-presenting/isolated-flow.md).
- **Persistence and auth** — The default configuration provided via the waltid-identity repo enables **SQLite persistence** (data survives restarts); **authentication is off** unless you enable the `auth` feature and configure [auth.conf](https://docs.walt.id/community-stack/wallet2/configurations/config-files/auth.md). Manage these via [Configurations](https://docs.walt.id/community-stack/wallet2/configurations/feature-manager.md) — e.g. switch persistence to in-memory.
## Next Steps
- [Setup](https://docs.walt.id/community-stack/wallet2/setup.md) — Run Wallet API v2 on your machine.
- [Managing Wallets](https://docs.walt.id/community-stack/wallet2/wallets.md) — Create and manage wallets and stores.
- [Key Management](https://docs.walt.id/community-stack/wallet2/key-management/overview.md) & [DID Management](https://docs.walt.id/community-stack/wallet2/did-management.md) — Manage the wallet's identity material.
- [Receive a credential](https://docs.walt.id/community-stack/wallet2/credential-receiving/sd-jwt-vc.md) — Claim what an issuer offers.
- [Present a credential](https://docs.walt.id/community-stack/wallet2/credential-presenting/sd-jwt-vc.md) — Share it with a verifier.
- [Configurations](https://docs.walt.id/community-stack/wallet2/configurations/feature-manager.md) — Persistence, authentication, and more.
--------------------------------------------------------------------------------
PAGE: Setup
URL: https://docs.walt.id/community-stack/wallet2/setup
RAW: https://docs.walt.id/community-stack/wallet2/setup.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Setup
## Installation & Running the Project
Make sure you have Docker installed on your machine.
**Note:**
Want to see the whole flow — issue a credential, receive it into the wallet, and present it? The [Docker Compose quick start](https://docs.walt.id/community-stack/home.md#quick-start) spins up the Issuer, Verifier, and Wallet together in one command. This page covers running the Wallet API v2 on its own.
### Option 1: Using Docker Pull
**1. Clone the Identity Repo**
Clone the repository to your local machine. The repo contains the default configuration files that will be mounted into the container in the next step.
```bash
git clone https://github.com/walt-id/waltid-identity.git && cd waltid-identity
```
**2. Pull the Docker Image**
Pull the pre-built Docker image from Docker Hub:
```bash
docker pull waltid/wallet-api2
```
**3. Run the Docker Image**
Run the container, mounting the config directory from the cloned repository. This gives the service its default configuration — see the note below for details.
```bash
docker run -p 7006:7006 \
-itv $(pwd)/waltid-services/waltid-wallet-api2/config:/waltid-wallet-api2/config \
-t waltid/wallet-api2
```
The `-v` flag mounts `waltid-services/waltid-wallet-api2/config` from the cloned repository into the container. This directory contains the service configuration files (e.g. server port, persistence, authentication). You can edit these files before starting the container to customize the service behavior.
**Note:**
See the [configuration reference](https://docs.walt.id/community-stack/wallet2/configurations/feature-manager.md) for a full description of all available config files and their fields. The shipped config enables **SQLite persistence** and leaves **authentication off** unless you enable and configure the `auth` feature.
Once the container starts, the API is available at `http://localhost:7006`. You can confirm it is running by opening `http://localhost:7006/swagger` in your browser.
### Option 2: Building the Image Yourself
**Note:**
This option requires **Java (JDK 17+)**, **Gradle**, and **Docker** to be installed on your machine.
**1. Clone the Identity Repo**
Clone the repository to your local machine:
```bash
git clone https://github.com/walt-id/waltid-identity.git && cd waltid-identity
```
**2. Build the Docker Image**
If you prefer to build the Docker image yourself, use the Gradle Ktor/Jib tasks:
```bash
# Development (local Docker daemon, single-arch)
./gradlew :waltid-services:waltid-wallet-api2:publishImageToLocalRegistry
# image: waltid/wallet-api2:{version}
```
```bash
# Production (multi-arch push to your registry)
export DOCKER_USERNAME={your-dockerhub-namespace}
export DOCKER_PASSWORD={your-dockerhub-token}
./gradlew :waltid-services:waltid-wallet-api2:publishImage
# image: docker.io/{DOCKER_USERNAME}/wallet-api2:{version}
```
**3. Run the Docker Image**
After building the image, run it using (adjust the image name if you pushed to your registry):
```bash
docker run -p 7006:7006 \
-itv $(pwd)/waltid-services/waltid-wallet-api2/config:/waltid-wallet-api2/config \
-t waltid/wallet-api2
```
The `-v` flag mounts `waltid-services/waltid-wallet-api2/config` from the cloned repository into the container. This directory contains the service configuration files (e.g. server port, persistence, authentication). You can edit these files before starting the container to customize the service behavior.
**Note:**
See the [configuration reference](https://docs.walt.id/community-stack/wallet2/configurations/feature-manager.md) for a full description of all available config files and their fields.
Once the container starts, the API is available at `http://localhost:7006`. You can confirm it is running by opening `http://localhost:7006/swagger` in your browser.
**Note:**
The Wallet API v2 listens on port `7006` by default — the same whether run standalone or via [Docker Compose](https://docs.walt.id/community-stack/home.md#quick-start) alongside the other services. Issuer2 defaults to `7005` and Verifier2 to `7004`.
## Start Using the Wallet
Now that the Wallet API v2 is running, create a wallet and put it to work:
1. [Create your first wallet](https://docs.walt.id/community-stack/wallet2/wallets.md) — Every operation is scoped to a wallet; start here to get a `walletId`.
2. [Manage keys](https://docs.walt.id/community-stack/wallet2/key-management/overview.md) & [DIDs](https://docs.walt.id/community-stack/wallet2/did-management.md) — The identity material the wallet signs with.
3. [Receive a credential](https://docs.walt.id/community-stack/wallet2/credential-receiving/sd-jwt-vc.md) — Claim what an issuer offers over OID4VCI.
4. [Present a credential](https://docs.walt.id/community-stack/wallet2/credential-presenting/sd-jwt-vc.md) — Share it with a verifier over OID4VP.
5. [Full API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md) — Explore all available endpoints via Swagger.
--------------------------------------------------------------------------------
PAGE: Managing Wallets
URL: https://docs.walt.id/community-stack/wallet2/wallets
RAW: https://docs.walt.id/community-stack/wallet2/wallets.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Managing Wallets
A **wallet** in the Wallet API v2 is a container for keys, credentials, and DIDs. This page covers creating wallets, inspecting and deleting them, and store-less wallets for isolated flows.
**Note:**
This page assumes the Wallet API v2 is already running. If it isn't, start with [Setup](https://docs.walt.id/community-stack/wallet2/setup.md) first.
**Note:**
**Local Development:** the API is available at `http://localhost:7006` by default, whether Wallet API v2 runs standalone or via the Docker Compose quick start.
---
## Create a Wallet
The simplest wallet uses default, auto-created stores for data persistence — one key store, one credential store, and one DID store, created automatically.
**Option: CURL**
**Endpoint:** `POST /wallet` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)
##### Example Request
```bash
curl -X POST http://localhost:7006/wallet \
-H 'Content-Type: application/json' \
-d '{}'
```
**Body Parameters**
All fields are optional. An empty body creates a wallet with default, auto-created stores.
- **noDidStore**: _Boolean_ (optional) - Create the wallet without a DID store. Only required if you want to receive, manage and share [W3C VC](https://docs.walt.id/concepts/digital-credentials/verifiable-credentials-w3c.md), for [SD-JWT VC](https://docs.walt.id/concepts/digital-credentials/sd-jwt-vc.md) and [mdoc](https://docs.walt.id/concepts/digital-credentials/mdoc-mdl-iso.md) based credentials this is not needed. Defaults to `false`.
- **keyStoreIds**: _String[]_ (optional) - Existing named key stores to attach to the wallet. Each store ID must already exist.
- **credentialStoreIds**: _String[]_ (optional) - Existing named credential stores to attach to the wallet. Each store ID must already exist.
- **didStoreId**: _String_ (optional) - Existing named DID store to attach to the wallet. The store ID must already exist.
- **staticKey**: _Object_ (optional) - A serialized key for a [store-less wallet](#store-less-wallets), used instead of a key store.
- **staticDid**: _String_ (optional) - A DID for a store-less wallet.
---
##### Example Response
```json
{
"walletId": "550e8400-e29b-41d4-a716-446655440000"
}
```
**Response Fields**
- **walletId**: _String_ - The new wallet's ID, used in every wallet-scoped call.
**Error:**
Stores, like key stores or DID stores, are attached at creation time only — **you cannot add or remove stores from a wallet after it is created.** Decide the store layout up front.
---
## Inspect and Delete Wallets
### List Wallets
**Option: CURL**
**Endpoint:** `GET /wallet` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)
##### Example Request
```bash
curl http://localhost:7006/wallet
```
---
##### Example Response
```json
["550e8400-e29b-41d4-a716-446655440000"]
```
When [authentication](https://docs.walt.id/community-stack/wallet2/configurations/config-files/auth.md) is enabled, this returns only the wallets owned by the authenticated account.
### Get Wallet Info
**Option: CURL**
**Endpoint:** `GET /wallet/{walletId}` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)
##### Example Request
```bash
curl http://localhost:7006/wallet/{walletId}
```
**Path Parameters**
- **walletId**: _String_ (required) - The wallet to inspect.
---
##### Example Response
```json
{
"walletId": "550e8400-e29b-41d4-a716-446655440000",
"keyStoreCount": 1,
"credentialStoreCount": 1,
"hasDidStore": true,
"hasStaticKey": false,
"hasStaticDid": false,
"keyStoreIds": ["550e8400-e29b-41d4-a716-446655440000-keys"],
"credentialStoreIds": ["550e8400-e29b-41d4-a716-446655440000-credentials"],
"didStoreId": "550e8400-e29b-41d4-a716-446655440000-dids",
"defaultKeyId": "UneoCeKrx7t0SBINBZQOaSCCzvhgpK8tRS3NEG4pCfU",
"defaultDidId": "did:key:z6Mkfriq1MqLBoPWecGoDLjguo1sB9brj6wT3qZ5BxkKpuP6"
}
```
**Response Fields**
- **walletId**: _String_ - The wallet ID.
- **keyStoreCount** / **credentialStoreCount**: _Number_ - How many stores of each type are attached.
- **hasDidStore**: _Boolean_ - Whether a DID store is attached.
- **hasStaticKey** / **hasStaticDid**: _Boolean_ - Whether the wallet uses a static key/DID (see [store-less wallets](#store-less-wallets)).
- **keyStoreIds** / **credentialStoreIds**: _String[]_ - Named key and credential store IDs attached to the wallet.
- **didStoreId**: _String_ (optional) - Named DID store ID attached to the wallet.
- **defaultKeyId**: _String_ (optional) - Key the wallet uses by default when `keyId` is omitted.
- **defaultDidId**: _String_ (optional) - DID the wallet uses by default when `did` is omitted.
### Delete a Wallet
**Option: CURL**
**Endpoint:** `DELETE /wallet/{walletId}` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)
##### Example Request
```bash
curl -X DELETE http://localhost:7006/wallet/{walletId}
```
**Path Parameters**
- **walletId**: _String_ (required) - The wallet to delete.
---
##### Example Response
`204 No Content` on success.
---
## Store-less Wallets
For isolated, one-off flows — signing a single presentation, or receiving a credential with externally managed key material — you can create a wallet with **no key or DID store**, backed by a static key and (optionally) a static DID:
```bash
curl -X POST http://localhost:7006/wallet \
-H 'Content-Type: application/json' \
-d '{
"staticKey": { "type": "jwk", "jwk": { "kty": "OKP", "crv": "Ed25519", "x": "...", "d": "..." } },
"staticDid": "did:key:z6Mkfriq1MqLBoPWecGoDLjguo1sB9brj6wT3qZ5BxkKpuP6",
"noDidStore": true
}'
```
The `staticKey` becomes the wallet's default key — it appears in the wallet's key list and is used for signing — and `staticDid` becomes its default DID, so credential [receiving](https://docs.walt.id/community-stack/wallet2/credential-receiving/overview.md) and [presenting](https://docs.walt.id/community-stack/wallet2/credential-presenting/overview.md) work without a key or DID store. You can't add further keys or DIDs (as there are no key or DID stores to manage/persist them), but a **credential store is still created automatically**, so received credentials are stored and can be listed as usual.
---
## Next Steps
- [Manage keys](https://docs.walt.id/community-stack/wallet2/key-management/overview.md) — generate, import, and list keys.
- [Manage DIDs](https://docs.walt.id/community-stack/wallet2/did-management.md) — create and import DIDs.
- [Receive a credential](https://docs.walt.id/community-stack/wallet2/credential-receiving/sd-jwt-vc.md) into the wallet.
- [Enable persistence](https://docs.walt.id/community-stack/wallet2/configurations/config-files/persistence.md) so wallet data survives restarts.
--------------------------------------------------------------------------------
PAGE: Overview
URL: https://docs.walt.id/community-stack/wallet2/key-management/overview
RAW: https://docs.walt.id/community-stack/wallet2/key-management/overview.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Key Management
Every credential a wallet holds is bound to a cryptographic **key**. The wallet signs proofs of possession (when receiving) and presentations (when presenting) with a key from its key store. This section covers generating, importing, and managing those keys.
## The Key Model
- A wallet can have more than one key when using **key stores** (see [Managing Wallets](https://docs.walt.id/community-stack/wallet2/wallets.md)).
- Each key has a **key ID** (derived from the key itself) and a **key type** (`Ed25519`, `secp256r1`, `secp256k1`, `secp384r1`, `secp521r1`, `RSA`, `RSA3072`, or `RSA4096`).
- Keys are added by **generating** a new one or **importing** existing key material.
## Default Key
Many endpoints — receiving, presenting, DID creation — accept an optional `keyId`. When you omit it, the wallet uses its **default key**.
By default, the wallet uses the first key across its key stores, then falls back to the `staticKey` for a [store-less wallet](https://docs.walt.id/community-stack/wallet2/wallets.md#store-less-wallets). You can override that choice with [Set the Default Key](https://docs.walt.id/community-stack/wallet2/key-management/local.md#set-the-default-key). After a default is set, receiving and presenting flows use that key unless a request passes `keyId`.
To control which key is used:
- set a default key with `PUT /wallet/{walletId}/keys/{keyId}/set-default`,
- pass `keyId` explicitly on each call, or
- use a store-less wallet with a single `staticKey`.
## Generate vs Import
- **[Generate](https://docs.walt.id/community-stack/wallet2/key-management/local.md)** — the wallet creates a new key of the type you choose.
- **[Import](https://docs.walt.id/community-stack/wallet2/key-management/local.md)** — you supply existing key material (a JWK) and the wallet stores it.
**Note:**
Keys are currently **local only** (JWKs stored in memory or in the chosen database if persistence is enabled). Support for external KMS backends (AWS KMS, Azure Key Vault, HashiCorp Vault, OCI Vault) is planned.
## Next Steps
- [Local Keys](https://docs.walt.id/community-stack/wallet2/key-management/local.md) — generate, import, list, and delete keys.
- [DID Management](https://docs.walt.id/community-stack/wallet2/did-management.md) — create DIDs from your keys.
--------------------------------------------------------------------------------
PAGE: Local Keys
URL: https://docs.walt.id/community-stack/wallet2/key-management/local
RAW: https://docs.walt.id/community-stack/wallet2/key-management/local.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Local Keys
Local keys are generated and stored by the wallet as JWKs in the configured database if [persistence](https://docs.walt.id/community-stack/wallet2/configurations/config-files/persistence.md) is enabled. This page covers generating a key, importing existing key material, and listing or deleting keys.
See [Key Management](https://docs.walt.id/community-stack/wallet2/key-management/overview.md) for how the wallet chooses a [default key](https://docs.walt.id/community-stack/wallet2/key-management/overview.md#default-key).
**Note:**
**Local Development:** the API is available at `http://localhost:7006` by default, whether Wallet API v2 runs standalone or via the Docker Compose quick start.
---
## Generate a Key
Create a new key in the wallet's key store.
**Option: CURL**
**Endpoint:** `POST /wallet/{walletId}/keys/generate` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)
##### Example Request
```bash
curl -X POST http://localhost:7006/wallet/{walletId}/keys/generate \
-H 'Content-Type: application/json' \
-d '{ "backend": "jwk", "keyType": "secp256r1" }'
```
**Path Parameters**
- **walletId**: _String_ (required) - The wallet to add the key to. See [Managing Wallets](https://docs.walt.id/community-stack/wallet2/wallets.md).
**Body Parameters**
- **backend**: _String_ (required) - The key backend.
- **keyType**: _String_ (optional) - One of `Ed25519`, `secp256r1`, `secp256k1`, `secp384r1`, `secp521r1`, `RSA`, `RSA3072`, `RSA4096`. Defaults to `secp256r1`.
**Error:**
Prefer `secp256r1` (ES256) unless you have a specific reason to use another type. The Issuer2 and Verifier2 quick-start examples (and the wallet-attestation flow, if enabled) advertise `ES256` only — an `Ed25519` (EdDSA) key will fail with `invalid_proof` when receiving, or be rejected outright by attestation providers that don't support OKP keys.
---
##### Example Response
```json
{
"keyId": "UneoCeKrx7t0SBINBZQOaSCCzvhgpK8tRS3NEG4pCfU",
"keyType": "secp256r1"
}
```
**Response Fields**
- **keyId**: _String_ - The new key's ID. Use it as `keyId` when receiving, presenting, or creating a DID.
- **keyType**: _String_ - The key type.
---
## Import a Key
Import existing key material as a JWK.
**Option: CURL**
**Endpoint:** `POST /wallet/{walletId}/keys/import` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)
##### Example Request
```bash
curl -X POST http://localhost:7006/wallet/{walletId}/keys/import \
-H 'Content-Type: application/json' \
-d '{
"key": {
"type": "jwk",
"jwk": { "kty": "EC", "crv": "P-256", "x": "...", "y": "...", "d": "..." }
}
}'
```
**Path Parameters**
- **walletId**: _String_ (required) - The wallet to import the key into.
**Body Parameters**
- **key**: _Object_ (required) - The serialized key. `type` is `jwk`, and `jwk` carries the JWK (include the private `d` component for a signing key).
---
##### Example Response
```json
{
"keyId": "UneoCeKrx7t0SBINBZQOaSCCzvhgpK8tRS3NEG4pCfU",
"keyType": "secp256r1"
}
```
---
## List Keys
List the metadata of all keys across the wallet's key stores.
**Option: CURL**
**Endpoint:** `GET /wallet/{walletId}/keys` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)
##### Example Request
```bash
curl http://localhost:7006/wallet/{walletId}/keys
```
**Path Parameters**
- **walletId**: _String_ (required) - The wallet whose keys you want to list.
---
##### Example Response
```json
[
{ "keyId": "UneoCeKrx7t0SBINBZQOaSCCzvhgpK8tRS3NEG4pCfU", "keyType": "secp256r1" }
]
```
---
## Get a Key
Fetch a single key's metadata.
**Option: CURL**
**Endpoint:** `GET /wallet/{walletId}/keys/{keyId}` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)
##### Example Request
```bash
curl http://localhost:7006/wallet/{walletId}/keys/{keyId}
```
**Path Parameters**
- **walletId**: _String_ (required) - The wallet holding the key.
- **keyId**: _String_ (required) - The key to fetch. Obtain it from [list keys](#list-keys) or the generate/import response.
---
##### Example Response
```json
{ "keyId": "UneoCeKrx7t0SBINBZQOaSCCzvhgpK8tRS3NEG4pCfU", "keyType": "secp256r1" }
```
---
## Delete a Key
**Option: CURL**
**Endpoint:** `DELETE /wallet/{walletId}/keys/{keyId}` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)
##### Example Request
```bash
curl -X DELETE http://localhost:7006/wallet/{walletId}/keys/{keyId}
```
**Path Parameters**
- **walletId**: _String_ (required) - The wallet holding the key.
- **keyId**: _String_ (required) - The key to delete.
---
##### Example Response
`204 No Content` on success, or `404 Not Found` if the key doesn't exist.
---
## Set the Default Key
Set the key the wallet uses when a receive, present, or DID creation request omits `keyId`.
**Option: CURL**
**Endpoint:** `PUT /wallet/{walletId}/keys/{keyId}/set-default` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)
##### Example Request
```bash
curl -X PUT http://localhost:7006/wallet/{walletId}/keys/{keyId}/set-default
```
**Path Parameters**
- **walletId**: _String_ (required) - The wallet holding the key.
- **keyId**: _String_ (required) - The key to make the wallet's default.
---
##### Example Response
`204 No Content` on success, or `404 Not Found` if the key doesn't exist in the wallet.
---
## Next Steps
- [Create a DID](https://docs.walt.id/community-stack/wallet2/did-management.md) from one of your keys.
- [Receive a credential](https://docs.walt.id/community-stack/wallet2/credential-receiving/sd-jwt-vc.md) bound to a key.
--------------------------------------------------------------------------------
PAGE: DID Management
URL: https://docs.walt.id/community-stack/wallet2/did-management
RAW: https://docs.walt.id/community-stack/wallet2/did-management.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# DID Management
A [Decentralized Identifier (DID)](https://docs.walt.id/concepts/decentralised-identifiers.md) gives a wallet a portable, verifiable identity that credentials can be bound to. This page covers creating a DID from a wallet key, importing an existing DID, and listing or deleting DIDs.
A wallet needs a **DID store** to manage DIDs — the default wallet setup includes one (see [Managing Wallets](https://docs.walt.id/community-stack/wallet2/wallets.md)).
**Note:**
**Local Development:** the API is available at `http://localhost:7006` by default, whether Wallet API v2 runs standalone or via the Docker Compose quick start.
## Default DID
Receiving and presenting credential endpoints accept an optional `did`. When you omit it, the wallet uses its **default DID**. By default, that is the first DID in the wallet's DID store, then the `staticDid` for a [store-less wallet](https://docs.walt.id/community-stack/wallet2/wallets.md#store-less-wallets). You can override it with [Set the Default DID](#set-the-default-did), or pass `did` explicitly on each call.
---
## Create a DID
Create a DID from a key in the wallet. If you omit `keyId`, the wallet's [default key](https://docs.walt.id/community-stack/wallet2/key-management/overview.md#default-key) is used.
Pass method-specific registrar arguments in `options` when the DID method needs them — for example `domain` and `path` for `did:web`.
**Option: CURL**
**Endpoint:** `POST /wallet/{walletId}/dids/create` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)
##### Example Request — did:key
```bash
curl -X POST http://localhost:7006/wallet/{walletId}/dids/create \
-H 'Content-Type: application/json' \
-d '{ "method": "key" }'
```
##### Example Request — did:web
```bash
curl -X POST http://localhost:7006/wallet/{walletId}/dids/create \
-H 'Content-Type: application/json' \
-d '{
"method": "web",
"options": {
"domain": "example.com",
"path": "/users/alice"
}
}'
```
**Path Parameters**
- **walletId**: _String_ (required) - The wallet to create the DID in. See [Managing Wallets](https://docs.walt.id/community-stack/wallet2/wallets.md).
**Body Parameters**
- **method**: _String_ (required) - The DID method to create. Supported: `key`, `jwk`, `web`, `cheqd`.
- **keyId**: _String_ (optional) - The key to derive the DID from. Defaults to the wallet's default key.
- **options**: _Object_ (optional) - Method-specific registrar arguments. Common keys:
- **did:web** — `domain` (required), `path` (optional, e.g. `/users/alice`)
- **did:key** — `useJwkJcsPub` (optional boolean, e.g. for EBSI)
---
##### Example Response — did:key
```json
{
"did": "did:key:z6Mkfriq1MqLBoPWecGoDLjguo1sB9brj6wT3qZ5BxkKpuP6",
"document": {
"@context": ["https://www.w3.org/ns/did/v1", "https://w3id.org/security/suites/jws-2020/v1"],
"id": "did:key:z6Mkfriq1MqLBoPWecGoDLjguo1sB9brj6wT3qZ5BxkKpuP6",
"verificationMethod": [
{
"id": "did:key:z6Mkfri...#z6Mkfri...",
"type": "JsonWebKey2020",
"controller": "did:key:z6Mkfriq1MqLBoPWecGoDLjguo1sB9brj6wT3qZ5BxkKpuP6",
"publicKeyJwk": { "kty": "OKP", "crv": "Ed25519", "x": "..." }
}
],
"assertionMethod": ["did:key:z6Mkfri...#z6Mkfri..."],
"authentication": ["did:key:z6Mkfri...#z6Mkfri..."]
}
}
```
##### Example Response — did:web
```json
{
"did": "did:web:example.com:users:alice",
"document": {
"@context": ["https://www.w3.org/ns/did/v1", "https://w3id.org/security/suites/jws-2020/v1"],
"id": "did:web:example.com:users:alice",
"verificationMethod": [
{
"id": "did:web:example.com:users:alice#...",
"type": "JsonWebKey2020",
"controller": "did:web:example.com:users:alice",
"publicKeyJwk": { "kty": "OKP", "crv": "Ed25519", "x": "..." }
}
],
"assertionMethod": ["did:web:example.com:users:alice#..."],
"authentication": ["did:web:example.com:users:alice#..."]
}
}
```
**Response Fields**
- **did**: _String_ - The newly created DID.
- **document**: _Object_ - The DID document. For `did:web`, host this document at the configured domain and path so the DID is publicly resolvable (Wallet API v2 stores it in the wallet but does not host it for you).
**Note:**
Already have a DID registered elsewhere? Bring it into the wallet with [Import a DID](#import-a-did).
---
## Import a DID
Import an existing DID and its document.
**Option: CURL**
**Endpoint:** `POST /wallet/{walletId}/dids/import` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)
##### Example Request
```bash
curl -X POST http://localhost:7006/wallet/{walletId}/dids/import \
-H 'Content-Type: application/json' \
-d '{
"did": "did:web:example.org",
"document": "{\"id\":\"did:web:example.org\",\"verificationMethod\":[]}"
}'
```
**Path Parameters**
- **walletId**: _String_ (required) - The wallet to import the DID into.
**Body Parameters**
- **did**: _String_ (required) - The DID to import.
- **document**: _String_ (required) - The DID document, as a JSON string.
---
##### Example Response
```json
{
"did": "did:web:example.org",
"document": { "id": "did:web:example.org", "verificationMethod": [] }
}
```
---
## List DIDs
**Option: CURL**
**Endpoint:** `GET /wallet/{walletId}/dids` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)
##### Example Request
```bash
curl http://localhost:7006/wallet/{walletId}/dids
```
**Path Parameters**
- **walletId**: _String_ (required) - The wallet whose DIDs you want to list.
---
##### Example Response
```json
[
{
"did": "did:key:z6Mkfriq1MqLBoPWecGoDLjguo1sB9brj6wT3qZ5BxkKpuP6",
"document": { "id": "did:key:z6Mkfriq1MqLBoPWecGoDLjguo1sB9brj6wT3qZ5BxkKpuP6" }
}
]
```
---
## Get a DID
**Option: CURL**
**Endpoint:** `GET /wallet/{walletId}/dids/{did}` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)
##### Example Request
```bash
curl http://localhost:7006/wallet/{walletId}/dids/did:key:z6Mkfriq1MqLBoPWecGoDLjguo1sB9brj6wT3qZ5BxkKpuP6
```
**Path Parameters**
- **walletId**: _String_ (required) - The wallet holding the DID.
- **did**: _String_ (required) - The DID to fetch. Obtain it from [list DIDs](#list-dids) or the create/import response.
---
##### Example Response
```json
{
"did": "did:key:z6Mkfriq1MqLBoPWecGoDLjguo1sB9brj6wT3qZ5BxkKpuP6",
"document": { "id": "did:key:z6Mkfriq1MqLBoPWecGoDLjguo1sB9brj6wT3qZ5BxkKpuP6" }
}
```
---
## Delete a DID
**Option: CURL**
**Endpoint:** `DELETE /wallet/{walletId}/dids/{did}` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)
##### Example Request
```bash
curl -X DELETE http://localhost:7006/wallet/{walletId}/dids/did:key:z6Mkfriq1MqLBoPWecGoDLjguo1sB9brj6wT3qZ5BxkKpuP6
```
**Path Parameters**
- **walletId**: _String_ (required) - The wallet holding the DID.
- **did**: _String_ (required) - The DID to delete. Obtain it from [list DIDs](#list-dids) or the create/import response.
---
##### Example Response
`204 No Content` on success, or `404 Not Found` if the DID doesn't exist.
---
## Set the Default DID
Set the DID the wallet uses when a receive or present request omits `did`.
**Option: CURL**
**Endpoint:** `PUT /wallet/{walletId}/dids/{did}/set-default` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)
##### Example Request
```bash
curl -X PUT http://localhost:7006/wallet/{walletId}/dids/did:key:z6Mkfriq1MqLBoPWecGoDLjguo1sB9brj6wT3qZ5BxkKpuP6/set-default
```
**Path Parameters**
- **walletId**: _String_ (required) - The wallet holding the DID.
- **did**: _String_ (required) - The DID to make the wallet's default.
---
##### Example Response
`204 No Content` on success, or `404 Not Found` if the DID doesn't exist in the wallet.
---
## Next Steps
- [Receive a credential](https://docs.walt.id/community-stack/wallet2/credential-receiving/sd-jwt-vc.md) bound to your DID.
- [Manage keys](https://docs.walt.id/community-stack/wallet2/key-management/overview.md) the DIDs are derived from.
--------------------------------------------------------------------------------
PAGE: Overview
URL: https://docs.walt.id/community-stack/wallet2/credential-receiving/overview
RAW: https://docs.walt.id/community-stack/wallet2/credential-receiving/overview.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Credential Receiving
The Wallet API v2 receives credentials from an issuer using [OpenID4VCI 1.0](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md). You hand the wallet a **credential offer** — typically an `openid-credential-offer://` URL from an issuer such as [Issuer2](https://docs.walt.id/community-stack/issuer2/getting-started.md) — and the wallet claims the credential and stores it.
## How It Works
Receiving a credential runs through five steps:
1. **Resolve the offer** — parse the offer and fetch the issuer's metadata.
2. **Get an access token** — exchange a pre-authorized code (or an authorization code) for a token.
3. **Sign a proof of possession** — prove the wallet controls the holder key.
4. **Fetch the credential** — request it from the issuer's credential endpoint.
5. **Store it** — the credential is saved to the wallet's credential store.
## Two Ways to Receive
- **Full flow** — a single call (`POST /wallet/{walletId}/credentials/receive`) runs all five steps for you. This is the recommended path, and what the credential guides below use.
- **[Isolated Flow](https://docs.walt.id/community-stack/wallet2/credential-receiving/isolated-flow.md)** — drive each step yourself. Use it for custom wallet UIs (resolve-offer returns issuer display and per-offer credential details for consent before accept), the authorization-code grant, deferred issuance, and advanced options like inline keys, client attestation, and storing fetched credentials with `storeInWallet`.
## Supported Formats
- [SD-JWT VC (IETF)](https://docs.walt.id/concepts/digital-credentials/sd-jwt-vc.md)
- [W3C VC](https://docs.walt.id/concepts/digital-credentials/verifiable-credentials-w3c.md) (JWT-signed)
- [ISO 18013-5 mDL](https://docs.walt.id/concepts/digital-credentials/mdoc-mdl-iso.md)
## Start Receiving
Choose the credential type that matches your use case:
- [SD-JWT VC via OID4VCI](https://docs.walt.id/community-stack/wallet2/credential-receiving/sd-jwt-vc.md)
- [W3C VC via OID4VCI](https://docs.walt.id/community-stack/wallet2/credential-receiving/w3c-vc.md)
- [mDL via OID4VCI](https://docs.walt.id/community-stack/wallet2/credential-receiving/mdl.md)
Need fine-grained control? See [Isolated Flow](https://docs.walt.id/community-stack/wallet2/credential-receiving/isolated-flow.md).
--------------------------------------------------------------------------------
PAGE: SD-JWT VC via OID4VCI
URL: https://docs.walt.id/community-stack/wallet2/credential-receiving/sd-jwt-vc
RAW: https://docs.walt.id/community-stack/wallet2/credential-receiving/sd-jwt-vc.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Receiving SD-JWT VC Credentials via OID4VCI
This guide shows you how to receive an [IETF SD-JWT VC](https://docs.walt.id/concepts/digital-credentials/sd-jwt-vc.md) credential into a wallet using the walt.id Wallet API v2. You'll take a credential offer — for example one created with [Issuer2](https://docs.walt.id/community-stack/issuer2/credential-issuance/sd-jwt-vc.md) — and claim it into a wallet in a single call, then confirm it was stored.
[**SD-JWT VC**](https://docs.walt.id/concepts/digital-credentials/sd-jwt-vc.md): An IETF credential format supporting selective disclosure, letting the holder reveal only specific claims when presenting.
[**OID4VCI**](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md): The protocol used to deliver the credential from the issuer to the wallet.
**Note:**
This page follows the **pre-authorized code flow**. For the authorization-code flow and for driving each step yourself (custom UIs, deferred issuance), see [Isolated Flow](https://docs.walt.id/community-stack/wallet2/credential-receiving/isolated-flow.md).
## Prerequisites
Before you begin, ensure you have:
- **Wallet API v2 running** — Follow the [Setup](https://docs.walt.id/community-stack/wallet2/setup.md) guide. The examples below use `http://localhost:7006` as the base URL (Issuer2 defaults to `7005`).
- **A wallet holding a key** — [Create a wallet](https://docs.walt.id/community-stack/wallet2/wallets.md), then [generate a key](https://docs.walt.id/community-stack/wallet2/key-management/local.md) into it (`secp256r1` recommended — see the warning on that page) — it becomes the wallet's [default key](https://docs.walt.id/community-stack/wallet2/key-management/overview.md#default-key). You'll need the wallet's `walletId`.
- **An SD-JWT VC credential offer** — For a full end-to-end run, issue one with the [Issuer2 SD-JWT VC guide](https://docs.walt.id/community-stack/issuer2/credential-issuance/sd-jwt-vc.md). The offer is an `openid-credential-offer://` URL.
---
## Receive the Credential
Hand the offer to the wallet. This single call resolves the offer, requests an access token, signs a proof of possession with the wallet's default key, fetches the credential, and stores it.
**Option: CURL**
**Endpoint:** `POST /wallet/{walletId}/credentials/receive` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)
##### Example Request
```bash
curl -X POST http://localhost:7006/wallet/{walletId}/credentials/receive \
-H 'Content-Type: application/json' \
-d '{
"offerUrl": "openid-credential-offer://?credential_offer_uri=http%3A%2F%2Flocalhost%3A7005%2Fopenid4vci%2Fcredential-offer%3Fid%3D48f1dc54-134c-4e10-b889-17f3c0a595bf"
}'
```
**Path Parameters**
- **walletId**: _String_ (required) - The ID of the wallet that will receive the credential. Returned when you [create a wallet](https://docs.walt.id/community-stack/wallet2/wallets.md#create-a-wallet); to look up an existing one, list your wallets with `GET /wallet`.
**Body Parameters**
- **Credential offer** (required) — provide exactly one of:
- **offerUrl**: _String_ - The OID4VCI credential offer URL, e.g. the `credentialOffer` value returned by Issuer2.
- **offerJson**: _Object_ - The credential offer as an inline JSON object, instead of a URL.
- **txCode**: _String_ (optional) - Transaction code (PIN) if the offer requires one. Check the offer, or [resolve it first](https://docs.walt.id/community-stack/wallet2/credential-receiving/isolated-flow.md#resolve-the-offer) to see `txCodeRequired`.
- **keyId**: _String_ (optional) - Key from the wallet's key store to bind the credential to. Defaults to the wallet's default key.
- **did**: _String_ (optional) - DID for holder binding. Defaults to the wallet's default DID.
- **clientId**: _String_ (optional) - The OAuth 2.0 `client_id` the wallet presents to the issuer's authorization server. Defaults to `eudiw-abca`, which works with issuers that don't require client registration.
---
##### Example Response
```json
{
"credentialIds": ["6ba7b810-9dad-11d1-80b4-00c04fd430c8"],
"deferredTransactionIds": {}
}
```
**Response Fields**
- **credentialIds**: _String[]_ - Wallet-assigned IDs of the credentials that were received and stored.
- **deferredTransactionIds**: _Object_ - Map of credential configuration ID → `transaction_id`, present only when the issuer defers issuance. See [deferred issuance](https://docs.walt.id/community-stack/wallet2/credential-receiving/isolated-flow.md#deferred-credentials).
🎉 You've received an SD-JWT VC credential into the wallet.
---
## Confirm It Was Stored
List the wallet's credentials to see the one you just received. This returns metadata only — no raw credential data.
**Option: CURL**
**Endpoint:** `GET /wallet/{walletId}/credentials` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)
##### Example Request
```bash
curl http://localhost:7006/wallet/{walletId}/credentials
```
**Path Parameters**
- **walletId**: _String_ (required) - The ID of the wallet whose credentials you want to list. Returned when you [create a wallet](https://docs.walt.id/community-stack/wallet2/wallets.md#create-a-wallet); to look up an existing one, list your wallets with `GET /wallet`.
---
##### Example Response
```json
[
{
"id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"format": "dc+sd-jwt",
"issuer": "did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2Iiwi...",
"addedAt": "2026-07-08T20:30:11.663155755Z"
}
]
```
**Response Fields**
- **id**: _String_ - The stored credential's ID. Use it with [Credential Management](https://docs.walt.id/community-stack/wallet2/credential-management.md) to view or delete it.
- **format**: _String_ - Credential format. For an SD-JWT VC this is `dc+sd-jwt`.
- **issuer**: _String_ (optional) - Issuer identifier when it can be determined — for the credential above, the issuer's `did:jwk`.
- **subject**: _String_ (optional) - Holder/subject identifier, when the credential carries one. Often absent (as above).
- **label**: _String_ (optional) - User-assigned label, when set.
- **addedAt**: _String_ - ISO-8601 timestamp of when the credential was stored.
Optional fields (`issuer`, `subject`, `label`) are omitted from the response when not present.
---
## Doing It Step by Step
The full-flow call above is the fastest path. If you're building a wallet UI and need to show intermediate state — resolve the offer, preview what's being issued, prompt for a PIN, handle deferred issuance — drive the isolated endpoints yourself:
`resolve-offer` → `request-token` → `sign-proof` → `fetch-credential`
Each step, its request/response shape, and the authorization-code variant are documented in [Isolated Flow](https://docs.walt.id/community-stack/wallet2/credential-receiving/isolated-flow.md).
---
## Next Steps
- **Present the credential you just received** — Try [Presenting SD-JWT VC Credentials](https://docs.walt.id/community-stack/wallet2/credential-presenting/sd-jwt-vc.md) to share it with [Verifier2](https://docs.walt.id/community-stack/verifier2/credential-verification/sd-jwt-vc-oid4vp.md).
- **Receive other credential types** — [W3C VC](https://docs.walt.id/community-stack/wallet2/credential-receiving/w3c-vc.md) or [mDL](https://docs.walt.id/community-stack/wallet2/credential-receiving/mdl.md).
- **Manage stored credentials** — [List, view, and delete](https://docs.walt.id/community-stack/wallet2/credential-management.md) credentials in the wallet.
--------------------------------------------------------------------------------
PAGE: W3C VC via OID4VCI
URL: https://docs.walt.id/community-stack/wallet2/credential-receiving/w3c-vc
RAW: https://docs.walt.id/community-stack/wallet2/credential-receiving/w3c-vc.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Receiving W3C VC Credentials via OID4VCI
This guide shows you how to receive a [W3C Verifiable Credential](https://docs.walt.id/concepts/digital-credentials/verifiable-credentials-w3c.md) (JWT-signed) into a wallet using the walt.id Wallet API v2. You'll take a credential offer — for example one created with [Issuer2](https://docs.walt.id/community-stack/issuer2/credential-issuance/w3c-vc.md) — and claim it into a wallet in a single call, then confirm it was stored.
[**W3C VC**](https://docs.walt.id/concepts/digital-credentials/verifiable-credentials-w3c.md): The W3C Verifiable Credentials data model. Here the credential is delivered as a JWT (`jwt_vc_json`).
[**OID4VCI**](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md): The protocol used to deliver the credential from the issuer to the wallet.
**Note:**
This page follows the **pre-authorized code flow**. For the authorization-code flow and for driving each step yourself (custom UIs, deferred issuance), see [Isolated Flow](https://docs.walt.id/community-stack/wallet2/credential-receiving/isolated-flow.md).
## Prerequisites
Before you begin, ensure you have:
- **Wallet API v2 running** — Follow the [Setup](https://docs.walt.id/community-stack/wallet2/setup.md) guide. The examples use `http://localhost:7006` as the base URL (Issuer2 defaults to `7005`).
- **A wallet holding a key and a DID** — [Create a wallet](https://docs.walt.id/community-stack/wallet2/wallets.md), then [generate a key](https://docs.walt.id/community-stack/wallet2/key-management/local.md) and [create a DID](https://docs.walt.id/community-stack/wallet2/did-management.md) from it (e.g. `did:key`, `did:jwk`, or `did:web`); both become the wallet's defaults. You'll need the wallet's `walletId`.
- **A W3C VC credential offer** — For a full end-to-end run, issue one with the [Issuer2 W3C VC guide](https://docs.walt.id/community-stack/issuer2/credential-issuance/w3c-vc.md). The offer is an `openid-credential-offer://` URL.
**Error:**
A W3C VC is bound to the holder's **DID**, and the wallet signs the OID4VCI proof with a **key** — so it needs both. Without a DID the issuer can't bind the credential subject and rejects the request with `Cannot find in context: subjectDid`. (SD-JWT VC and mDL need only a key.)
---
## Receive the Credential
Hand the offer to the wallet. This single call resolves the offer, requests a token, signs a proof of possession with the wallet's default key, fetches the credential, and stores it.
**Option: CURL**
**Endpoint:** `POST /wallet/{walletId}/credentials/receive` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)
##### Example Request
```bash
curl -X POST http://localhost:7006/wallet/{walletId}/credentials/receive \
-H 'Content-Type: application/json' \
-d '{
"offerUrl": "openid-credential-offer://?credential_offer_uri=http%3A%2F%2Flocalhost%3A7005%2Fopenid4vci%2Fcredential-offer%3Fid%3D48f1dc54-134c-4e10-b889-17f3c0a595bf"
}'
```
**Path Parameters**
- **walletId**: _String_ (required) - The ID of the wallet that will receive the credential. Returned when you [create a wallet](https://docs.walt.id/community-stack/wallet2/wallets.md#create-a-wallet); to look up an existing one, list your wallets with `GET /wallet`.
**Body Parameters**
- **Credential offer** (required) — provide exactly one of:
- **offerUrl**: _String_ - The OID4VCI credential offer URL, e.g. the `credentialOffer` value returned by Issuer2.
- **offerJson**: _Object_ - The credential offer as an inline JSON object, instead of a URL.
- **txCode**: _String_ (optional) - Transaction code (PIN) if the offer requires one. Check the offer, or [resolve it first](https://docs.walt.id/community-stack/wallet2/credential-receiving/isolated-flow.md#resolve-the-offer) to see `txCodeRequired`.
- **keyId**: _String_ (optional) - Key from the wallet's key store to bind the credential to. Defaults to the wallet's default key.
- **did**: _String_ (optional) - DID for holder binding. Defaults to the wallet's default DID.
- **clientId**: _String_ (optional) - The OAuth 2.0 `client_id` the wallet presents to the issuer's authorization server. Defaults to `eudiw-abca`, which works with issuers that don't require client registration.
---
##### Example Response
```json
{
"credentialIds": ["6ba7b810-9dad-11d1-80b4-00c04fd430c8"],
"deferredTransactionIds": {}
}
```
**Response Fields**
- **credentialIds**: _String[]_ - Wallet-assigned IDs of the credentials that were received and stored.
- **deferredTransactionIds**: _Object_ - Map of credential configuration ID → `transaction_id`, present only when the issuer defers issuance. See [deferred issuance](https://docs.walt.id/community-stack/wallet2/credential-receiving/isolated-flow.md#deferred-credentials).
🎉 You've received a W3C VC credential into the wallet.
---
## Confirm It Was Stored
List the wallet's credentials to see the one you just received. This returns metadata only — no raw credential data.
**Option: CURL**
**Endpoint:** `GET /wallet/{walletId}/credentials` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)
##### Example Request
```bash
curl http://localhost:7006/wallet/{walletId}/credentials
```
**Path Parameters**
- **walletId**: _String_ (required) - The ID of the wallet whose credentials you want to list. Returned when you [create a wallet](https://docs.walt.id/community-stack/wallet2/wallets.md#create-a-wallet); to look up an existing one, list your wallets with `GET /wallet`.
---
##### Example Response
```json
[
{
"id": "5ea45ef4-19aa-40bc-9746-96bb9d3893ec",
"format": "jwt_vc_json",
"issuer": "did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2Ii...",
"subject": "did:key:zDnaeyvVjxHZwnWBb8gLxnHhJLM6TcRZfyCzs35DHMGk39V4f",
"addedAt": "2026-07-08T20:35:24.653545720Z"
}
]
```
**Response Fields**
- **id**: _String_ - The stored credential's ID. Use it with [Credential Management](https://docs.walt.id/community-stack/wallet2/credential-management.md) to view or delete it.
- **format**: _String_ - Credential format, `jwt_vc_json` for a JWT-signed W3C VC.
- **issuer**: _String_ (optional) - Issuer identifier — here the issuer's `did:jwk`.
- **subject**: _String_ (optional) - Holder DID the credential is bound to. For a W3C VC this is your wallet's DID from the prerequisites above.
- **label**: _String_ (optional) - User-assigned label, when set.
- **addedAt**: _String_ - ISO-8601 timestamp of when the credential was stored.
Optional fields (`issuer`, `subject`, `label`) are omitted when not present.
---
## Doing It Step by Step
The full-flow call above is the fastest path. If you're building a wallet UI and need to show intermediate state — resolve the offer, preview what's being issued, prompt for a PIN, handle the authorization-code flow — drive the isolated endpoints yourself. Each step and its request/response shape is documented in [Isolated Flow](https://docs.walt.id/community-stack/wallet2/credential-receiving/isolated-flow.md).
---
## Next Steps
- **Present the credential you just received** — Try [Presenting W3C VC Credentials](https://docs.walt.id/community-stack/wallet2/credential-presenting/w3c-vc.md) to share it with [Verifier2](https://docs.walt.id/community-stack/verifier2/credential-verification/vc-oid4vp.md).
- **Receive other credential types** — [SD-JWT VC](https://docs.walt.id/community-stack/wallet2/credential-receiving/sd-jwt-vc.md) or [mDL](https://docs.walt.id/community-stack/wallet2/credential-receiving/mdl.md).
- **Manage stored credentials** — [List, view, and delete](https://docs.walt.id/community-stack/wallet2/credential-management.md) credentials in the wallet.
--------------------------------------------------------------------------------
PAGE: mDL via OID4VCI
URL: https://docs.walt.id/community-stack/wallet2/credential-receiving/mdl
RAW: https://docs.walt.id/community-stack/wallet2/credential-receiving/mdl.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Receiving mDL Credentials via OID4VCI
This guide shows you how to receive an [ISO 18013-5 mobile driving licence (mDL)](https://docs.walt.id/concepts/digital-credentials/mdoc-mdl-iso.md) into a wallet using the walt.id Wallet API v2. You'll take a credential offer — for example one created with [Issuer2](https://docs.walt.id/community-stack/issuer2/credential-issuance/mdl.md) — and claim it into a wallet in a single call, then confirm it was stored.
[**mDL / mdoc**](https://docs.walt.id/concepts/digital-credentials/mdoc-mdl-iso.md): The ISO/IEC 18013-5 mobile document format (`mso_mdoc`), used for mobile driving licences and other ISO-standard credentials.
[**OID4VCI**](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md): The protocol used to deliver the credential from the issuer to the wallet.
**Note:**
This page follows the **pre-authorized code flow**. For the authorization-code flow and for driving each step yourself (custom UIs, deferred issuance), see [Isolated Flow](https://docs.walt.id/community-stack/wallet2/credential-receiving/isolated-flow.md).
## Prerequisites
Before you begin, ensure you have:
- **Wallet API v2 running** — Follow the [Setup](https://docs.walt.id/community-stack/wallet2/setup.md) guide. The examples use `http://localhost:7006` as the base URL (Issuer2 defaults to `7005`).
- **A wallet holding a `secp256r1` key** — [Create a wallet](https://docs.walt.id/community-stack/wallet2/wallets.md), then [generate a `secp256r1` key](https://docs.walt.id/community-stack/wallet2/key-management/local.md) into it (ES256, the curve mdoc/ISO 18013-5 expects) — it becomes the wallet's default key. You'll need the wallet's `walletId`.
- **An mDL credential offer** — For a full end-to-end run, issue one with the [Issuer2 mDL guide](https://docs.walt.id/community-stack/issuer2/credential-issuance/mdl.md). The offer is an `openid-credential-offer://` URL.
---
## Receive the Credential
Hand the offer to the wallet. This single call resolves the offer, requests a token, signs a proof of possession with the wallet's default key, fetches the credential, and stores it.
**Option: CURL**
**Endpoint:** `POST /wallet/{walletId}/credentials/receive` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)
##### Example Request
```bash
curl -X POST http://localhost:7006/wallet/{walletId}/credentials/receive \
-H 'Content-Type: application/json' \
-d '{
"offerUrl": "openid-credential-offer://?credential_offer_uri=http%3A%2F%2Flocalhost%3A7005%2Fopenid4vci%2Fcredential-offer%3Fid%3D48f1dc54-134c-4e10-b889-17f3c0a595bf"
}'
```
**Path Parameters**
- **walletId**: _String_ (required) - The ID of the wallet that will receive the credential. Returned when you [create a wallet](https://docs.walt.id/community-stack/wallet2/wallets.md#create-a-wallet); to look up an existing one, list your wallets with `GET /wallet`.
**Body Parameters**
- **Credential offer** (required) — provide exactly one of:
- **offerUrl**: _String_ - The OID4VCI credential offer URL, e.g. the `credentialOffer` value returned by Issuer2.
- **offerJson**: _Object_ - The credential offer as an inline JSON object, instead of a URL.
- **txCode**: _String_ (optional) - Transaction code (PIN) if the offer requires one. Check the offer, or [resolve it first](https://docs.walt.id/community-stack/wallet2/credential-receiving/isolated-flow.md#resolve-the-offer) to see `txCodeRequired`.
- **keyId**: _String_ (optional) - Key from the wallet's key store to bind the credential to. Defaults to the wallet's default key.
- **did**: _String_ (optional) - DID for holder binding. Defaults to the wallet's default DID. Not needed for mdoc credentials.
- **clientId**: _String_ (optional) - The OAuth 2.0 `client_id` the wallet presents to the issuer's authorization server. Defaults to `eudiw-abca`, which works with issuers that don't require client registration.
---
##### Example Response
```json
{
"credentialIds": ["6ba7b810-9dad-11d1-80b4-00c04fd430c8"],
"deferredTransactionIds": {}
}
```
**Response Fields**
- **credentialIds**: _String[]_ - Wallet-assigned IDs of the credentials that were received and stored.
- **deferredTransactionIds**: _Object_ - Map of credential configuration ID → `transaction_id`, present only when the issuer defers issuance. See [deferred issuance](https://docs.walt.id/community-stack/wallet2/credential-receiving/isolated-flow.md#deferred-credentials).
🎉 You've received an mDL credential into the wallet.
---
## Confirm It Was Stored
List the wallet's credentials to see the one you just received. This returns metadata only — no raw credential data.
**Option: CURL**
**Endpoint:** `GET /wallet/{walletId}/credentials` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)
##### Example Request
```bash
curl http://localhost:7006/wallet/{walletId}/credentials
```
**Path Parameters**
- **walletId**: _String_ (required) - The ID of the wallet whose credentials you want to list. Returned when you [create a wallet](https://docs.walt.id/community-stack/wallet2/wallets.md#create-a-wallet); to look up an existing one, list your wallets with `GET /wallet`.
---
##### Example Response
```json
[
{
"id": "afbfe84f-839b-40ef-bf0c-8a579e433ba8",
"format": "mso_mdoc",
"addedAt": "2026-07-08T20:35:47.770818300Z"
}
]
```
**Response Fields**
- **id**: _String_ - The stored credential's ID. Use it with [Credential Management](https://docs.walt.id/community-stack/wallet2/credential-management.md) to view or delete it.
- **format**: _String_ - Credential format, `mso_mdoc` for an ISO 18013-5 mdoc.
- **addedAt**: _String_ - ISO-8601 timestamp of when the credential was stored.
For an mdoc the optional `issuer`, `subject`, and `label` fields are not populated (mdoc binds to a device key, not a subject DID), so they're omitted from the response.
---
## Doing It Step by Step
The full-flow call above is the fastest path. If you're building a wallet UI and need to show intermediate state — resolve the offer, preview what's being issued, prompt for a PIN, handle the authorization-code flow — drive the isolated endpoints yourself. Each step and its request/response shape is documented in [Isolated Flow](https://docs.walt.id/community-stack/wallet2/credential-receiving/isolated-flow.md).
---
## Next Steps
- **Present the credential you just received** — Try [Presenting mDL Credentials](https://docs.walt.id/community-stack/wallet2/credential-presenting/mdl.md) to share it with [Verifier2](https://docs.walt.id/community-stack/verifier2/credential-verification/mdl-oid4vp.md).
- **Receive other credential types** — [SD-JWT VC](https://docs.walt.id/community-stack/wallet2/credential-receiving/sd-jwt-vc.md) or [W3C VC](https://docs.walt.id/community-stack/wallet2/credential-receiving/w3c-vc.md).
- **Manage stored credentials** — [List, view, and delete](https://docs.walt.id/community-stack/wallet2/credential-management.md) credentials in the wallet.
--------------------------------------------------------------------------------
PAGE: Isolated Flow
URL: https://docs.walt.id/community-stack/wallet2/credential-receiving/isolated-flow
RAW: https://docs.walt.id/community-stack/wallet2/credential-receiving/isolated-flow.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Isolated Flow
The [full-flow endpoint](https://docs.walt.id/community-stack/wallet2/credential-receiving/overview.md) runs the whole [**OID4VCI**](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md) exchange in a single call. When you're building a wallet UI and need to show intermediate state, prompt for a PIN, support the authorization-code grant, or handle deferred issuance, drive the **isolated endpoints** explained on this page yourself.
[**OID4VCI**](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md): The protocol used to deliver the credential from the issuer to the wallet.
[Resolve the offer](#resolve-the-offer) returns a **consent-oriented** preview — issuer branding and per-offer credential details — so UIs can show what will be issued before accepting.
All endpoints on this page are rooted at `/wallet/{walletId}/credentials/receive`. The examples use `http://localhost:7006` as the base URL (Issuer2 defaults to `7005`).
## Prerequisites
Before you begin, ensure you have:
- **Wallet API v2 running** — Follow the [Setup](https://docs.walt.id/community-stack/wallet2/setup.md) guide.
- **A wallet holding a key** — [Create a wallet](https://docs.walt.id/community-stack/wallet2/wallets.md), then [generate a key](https://docs.walt.id/community-stack/wallet2/key-management/local.md) into it. You'll need the wallet's `walletId`.
- **A credential offer** — Use an `openid-credential-offer://` URL or inline offer JSON from an issuer such as [Issuer2](https://docs.walt.id/community-stack/issuer2/getting-started.md).
---
## When to Use the Isolated Flow
Generally speaking, you should use the isolated flow when building any form of wallet user interface leveraging the wallet API v2. This allows you to properly step through the credential receiving process and build the necessary UI elements accordingly.
| You want to… | Use |
|---|---|
| Claim a credential in one call | [Full flow](https://docs.walt.id/community-stack/wallet2/credential-receiving/overview.md) |
| Preview the offer before claiming | [Resolve the offer](#resolve-the-offer) first |
| Issuer requires user auth before claiming | [Authorization-code flow](#authorization-code-flow) |
| Poll for a credential issued later | [Deferred credentials](#deferred-credentials) |
| Manage key material outside the wallet | [Inline keys](#inline-keys) |
---
## Pre-Authorized Code Flow
The isolated pre-authorized flow is:
```text
1. Resolve Offer → 2. Request Token → 2b. Get Nonce → 3. Sign Proof → 4. Fetch Credential
```
### Resolve the Offer
Parse a credential offer and fetch the issuer metadata needed for a consent preview and the later steps.
**Option: CURL**
**Endpoint:** `POST /wallet/{walletId}/credentials/receive/resolve-offer` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)
##### Example Request
```bash
curl -X POST http://localhost:7006/wallet/{walletId}/credentials/receive/resolve-offer \
-H 'Content-Type: application/json' \
-d '{
"offerUrl": "openid-credential-offer://?credential_offer_uri=http%3A%2F%2Flocalhost%3A7005%2Fopenid4vci%2Fcredential-offer%3Fid%3D48f1dc54-134c-4e10-b889-17f3c0a595bf"
}'
```
**Path Parameters**
- **walletId**: _String_ (required) - The wallet handling the exchange. See [how to obtain it](https://docs.walt.id/community-stack/wallet2/wallets.md#create-a-wallet).
**Body Parameters**
- **Credential offer** (required) — provide exactly one of:
- **offerUrl**: _String_ - The OID4VCI credential offer URL.
- **offerJson**: _Object_ - The credential offer as an inline JSON object.
---
##### Example Response
```json
{
"credentialIssuer": "http://localhost:7005/openid4vci",
"credentialConfigurationIds": ["identity_credential"],
"grantType": "pre-authorized_code",
"preAuthorizedCode": "SplxlOBeZQQYbYS6WxSbIA",
"txCodeRequired": true,
"credentialEndpoint": "http://localhost:7005/openid4vci/credential",
"tokenEndpoint": "http://localhost:7005/openid4vci/token",
"nonceEndpoint": "http://localhost:7005/openid4vci/nonce",
"issuer": {
"credentialIssuer": "http://localhost:7005/openid4vci",
"display": {
"name": "Example Issuer",
"locale": "en-US",
"logoUri": "https://issuer.example.com/logo.png",
"logoAltText": "Example Issuer"
}
},
"offeredCredentials": [
{
"configurationId": "identity_credential",
"format": "dc+sd-jwt",
"scope": null,
"vct": "https://issuer.example.com/identity_credential",
"doctype": null,
"display": {
"name": "Identity Credential",
"locale": "en-US",
"description": "Personal identity claims"
},
"claims": [
{
"path": ["given_name"],
"mandatory": true,
"displayName": "Given name"
},
{
"path": ["family_name"],
"mandatory": false,
"displayName": "Family name"
}
]
}
],
"transactionCode": {
"inputMode": "numeric",
"length": 6,
"description": "Enter the PIN you received from the issuer"
}
}
```
**Response Fields**
- **credentialIssuer**: _String_ - Issuer identifier from the offer.
- **credentialConfigurationIds**: _String[]_ - Flat list of credential configuration IDs listed in the offer.
- **grantType**: _String_ - `pre-authorized_code` or `authorization_code`.
- **preAuthorizedCode**: _String_ (optional) - The pre-authorized code from the credential offer, returned when the offer uses the pre-authorized code grant.
- **txCodeRequired**: _Boolean_ - Whether the pre-authorized flow requires a transaction code (PIN).
- **credentialEndpoint**: _String_ - The issuer's credential endpoint, from its metadata.
- **tokenEndpoint**: _String_ (optional) - The token endpoint from the issuer's authorization-server metadata.
- **nonceEndpoint**: _String_ (optional) - The issuer's nonce endpoint for `c_nonce` flows (OID4VCI 1.0).
- **issuer**: _Object_ - Issuer metadata projection for consent UIs (`credentialIssuer`, optional localized `display` with `name`, `locale`, `logoUri`, `logoAltText`, and related branding fields when present).
- **offeredCredentials**: _Object[]_ - Per-configuration offer details for the UI (not strings). Each entry includes:
- **configurationId** / **format** — configuration id and credential format.
- **scope** / **vct** / **doctype** — format-specific identifiers when present.
- **display** — localized credential display (name, description, colors, logo, …).
- **claims** — declared claim metadata (`path`, `mandatory`, `displayName`).
- **transactionCode**: _Object_ (optional) - UI hints for collecting a transaction code when required (`inputMode` `numeric` or `text`, optional `length`, optional `description`). Distinct from the PIN value you send later as `txCode`.
### Request Token
Exchange the pre-authorized code for an access token.
**Option: CURL**
**Endpoint:** `POST /wallet/{walletId}/credentials/receive/request-token` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)
##### Example Request
```bash
curl -X POST http://localhost:7006/wallet/{walletId}/credentials/receive/request-token \
-H 'Content-Type: application/json' \
-d '{
"tokenEndpoint": "http://localhost:7005/openid4vci/token",
"preAuthorizedCode": "SplxlOBeZQQYbYS6WxSbIA",
"credentialIssuer": "http://localhost:7005/openid4vci",
"txCode": "123456"
}'
```
**Body Parameters**
- **tokenEndpoint**: _String_ (required) - The issuer's token endpoint.
- **preAuthorizedCode**: _String_ (required) - The pre-authorized code from the credential offer.
- **credentialIssuer**: _String_ (optional) - Credential issuer base URL. When supplied, the wallet resolves authorization-server metadata and can apply client-attestation headers if configured.
- **txCode**: _String_ (optional) - Transaction code (PIN), if the offer requires one.
- **clientId**: _String_ (optional) - See [OAuth client identity](#oauth-client-identity). Defaults to `eudiw-abca`.
- **redirectUri**: _String_ (optional) - Defaults to `openid://`.
- **tokenRequestHeaders**: _Object_ (optional) - Additional headers to attach to the token request, such as `OAuth-Client-Attestation` and `OAuth-Client-Attestation-PoP`.
- **anonymousPreAuthorizedCode**: _Boolean_ (optional) - Request an anonymous pre-authorized-code token exchange when the issuer supports it. Defaults to `false`.
---
##### Example Response
```json
{
"accessToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"expiresIn": 300
}
```
**Response Fields**
- **accessToken**: _String_ - Use it in the fetch-credential request.
- **cNonce**: _String_ (optional) - Proof nonce, **only if** the issuer returns one in the token response. Under OID4VCI 1.0 the issuer (including Issuer2) serves the nonce from a dedicated **Nonce Endpoint** instead, so this is usually absent — fetch it in [Get a Nonce](#get-a-nonce) below.
- **expiresIn**: _Number_ - Token lifetime in seconds.
**Note:**
Under OID4VCI 1.0 the proof nonce (`c_nonce`) comes from the issuer's **Nonce Endpoint**, not the token response — so `request-token` typically returns no `cNonce`. Get it via [Get a Nonce](#get-a-nonce) before signing the proof.
### Get a Nonce
Under OID4VCI 1.0 the proof `c_nonce` comes from the issuer's dedicated **Nonce Endpoint**, not the token response. The wallet does not proxy it — call the **issuer's** nonce endpoint directly. Its URL is returned as **`nonceEndpoint`** from [resolve-offer](#resolve-the-offer) when the issuer publishes one (otherwise use the `nonce_endpoint` from the issuer's credential-issuer metadata; for [Issuer2](https://docs.walt.id/community-stack/issuer2/getting-started.md) it is `{credentialIssuer}/nonce`).
**Option: CURL**
**Endpoint:** `POST {credentialIssuer}/nonce` — the **issuer's** endpoint, not a wallet endpoint.
##### Example Request
```bash
curl -X POST http://localhost:7005/openid4vci/nonce
```
---
##### Example Response
```json
{
"c_nonce": "wKI4LT5Sadf6a0dyih5b0Ws"
}
```
Use the returned `c_nonce` as the `nonce` in [Sign Proof](#sign-proof).
### Sign Proof
Sign a proof-of-possession JWT that proves the wallet controls the holder key.
**Option: CURL**
**Endpoint:** `POST /wallet/{walletId}/credentials/receive/sign-proof` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)
##### Example Request
```bash
curl -X POST http://localhost:7006/wallet/{walletId}/credentials/receive/sign-proof \
-H 'Content-Type: application/json' \
-d '{
"issuerUrl": "http://localhost:7005/openid4vci",
"nonce": "wKI4LT5Sadf6a0dyih5b0Ws",
"credentialConfigurationId": "identity_credential"
}'
```
**Body Parameters**
- **issuerUrl**: _String_ (required) - The credential issuer URL, used as the proof audience.
- **nonce**: _String_ (required) - The `c_nonce` from [Get a Nonce](#get-a-nonce) (or from the token response, if the issuer returned one).
- **credentialConfigurationId**: _String_ (required) - The credential configuration ID being requested.
- **keyId**: _String_ (optional) - Key from the wallet's key store. Defaults to the wallet's default key.
- **did**: _String_ (optional) - DID to use as the proof key ID. If omitted, the proof carries the JWK directly.
- **key**: _Object_ (optional) - An [inline key](#inline-keys). Takes precedence over `keyId`.
---
##### Example Response
```json
{
"proofJwt": "eyJhbGciOiJFUzI1NiIsInR5cCI6Im9wZW5pZDR2Y2ktcHJvb2Yrand0In0..."
}
```
### Fetch Credential
Request the credential from the issuer's credential endpoint.
**Option: CURL**
**Endpoint:** `POST /wallet/{walletId}/credentials/receive/fetch-credential` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)
##### Example Request
```bash
curl -X POST http://localhost:7006/wallet/{walletId}/credentials/receive/fetch-credential \
-H 'Content-Type: application/json' \
-d '{
"credentialEndpoint": "http://localhost:7005/openid4vci/credential",
"accessToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"credentialConfigurationId": "identity_credential",
"proofJwt": "eyJhbGciOiJFUzI1NiIsInR5cCI6Im9wZW5pZDR2Y2ktcHJvb2Yrand0In0...",
"storeInWallet": true
}'
```
**Body Parameters**
- **credentialEndpoint**: _String_ (required) - The issuer's credential endpoint.
- **accessToken**: _String_ (required) - The access token from the token response.
- **credentialConfigurationId**: _String_ (required) - The credential configuration ID to request.
- **proofJwt**: _String_ (optional) - The proof JWT from the sign-proof step. Required if the issuer requires proof.
- **clientId**: _String_ (optional) - See [OAuth client identity](#oauth-client-identity). Defaults to `eudiw-abca`.
- **storeInWallet**: _Boolean_ (optional) - Store the fetched credential(s) in the wallet credential store. Defaults to `false`.
---
##### Example Response
```json
{
"rawCredentials": [
"eyJraWQiOiJkaWQ6a2V5Ono2TWtmcmlxMU1xTEJvUFdlY0dvRExqZ3VvMXNCOWJyajZ3VDNxWjVCeGtLcHVQNiJ9..."
]
}
```
**Note:**
The isolated fetch endpoint always returns raw credential strings. Set `storeInWallet` to `true` to store them during this call. If you leave it as `false`, store a returned credential later with `POST /wallet/{walletId}/credentials/import`, or use the [full flow](https://docs.walt.id/community-stack/wallet2/credential-receiving/overview.md), which stores automatically.
---
## Authorization-Code Flow
When the issuer requires the holder to log in first, use the authorization-code grant. It replaces the "request token" step with a redirect through the issuer's authorization server.
```text
1. Resolve Offer → 2. Generate Authorization URL → (user logs in, issuer redirects back)
→ 3. Exchange Code → 4. Sign Proof → 5. Fetch Credential
```
Steps 1, 4 and 5 are identical to the pre-authorized flow above.
### Generate Authorization URL
Build the authorization URL to redirect the user to.
**Option: CURL**
**Endpoint:** `POST /wallet/{walletId}/credentials/receive/authorization-url` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)
##### Example Request
```bash
curl -X POST http://localhost:7006/wallet/{walletId}/credentials/receive/authorization-url \
-H 'Content-Type: application/json' \
-d '{
"offerUrl": "openid-credential-offer://?credential_offer_uri=...",
"redirectUri": "https://wallet.example.org/callback",
"usePkce": true
}'
```
**Body Parameters**
- **Credential offer** (required) — provide exactly one of:
- **offerUrl**: _String_ - The OID4VCI credential offer URL.
- **offerJson**: _Object_ - The credential offer as an inline JSON object.
- **redirectUri**: _String_ (optional) - Redirect URI registered with the issuer. Defaults to `openid://`.
- **usePkce**: _Boolean_ (optional) - Whether to use PKCE. Defaults to `true`.
- **clientId**: _String_ (optional) - See [OAuth client identity](#oauth-client-identity). Defaults to `eudiw-abca`.
---
##### Example Response
```json
{
"authorizationUrl": "http://localhost:7005/openid4vci/authorize?response_type=code&client_id=eudiw-abca&state=abc123&code_challenge=...",
"state": "abc123",
"codeVerifier": "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
"credentialConfigurationId": "identity_credential",
"credentialIssuerBaseUrl": "http://localhost:7005/openid4vci",
"nonceEndpoint": "http://localhost:7005/openid4vci/nonce"
}
```
**Response Fields**
- **authorizationUrl**: _String_ - Redirect the user here.
- **state**: _String_ - Returned to your redirect URI; use it to correlate the response.
- **codeVerifier**: _String_ - Store it — you'll need it to exchange the code (PKCE).
- **credentialConfigurationId**: _String_ - The credential being requested.
- **credentialIssuerBaseUrl**: _String_ - Credential issuer base URL. Pass this to [Exchange Code](#exchange-code).
- **nonceEndpoint**: _String_ (optional) - The issuer's nonce endpoint for `c_nonce` flows (OID4VCI 1.0), when published. See [Get a Nonce](#get-a-nonce).
After the user authenticates, the issuer redirects to your `redirectUri` with `code` and `state` query parameters.
### Exchange Code
Exchange the authorization code for an access token.
**Option: CURL**
**Endpoint:** `POST /wallet/{walletId}/credentials/receive/exchange-code` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)
##### Example Request
```bash
curl -X POST http://localhost:7006/wallet/{walletId}/credentials/receive/exchange-code \
-H 'Content-Type: application/json' \
-d '{
"code": "SplxlOBeZQQYbYS6WxSbIA",
"credentialIssuerBaseUrl": "http://localhost:7005/openid4vci",
"codeVerifier": "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
"redirectUri": "https://wallet.example.org/callback"
}'
```
**Body Parameters**
- **code**: _String_ (required) - The authorization code returned to your redirect URI.
- **credentialIssuerBaseUrl**: _String_ (required) - Credential issuer base URL returned by [Generate Authorization URL](#generate-authorization-url). The wallet uses it to resolve authorization-server metadata, including the token endpoint.
- **codeVerifier**: _String_ (optional) - The PKCE code verifier from the authorization-url step.
- **redirectUri**: _String_ (optional) - The redirect URI used in the authorization request. Defaults to `openid://`.
- **clientId**: _String_ (optional) - See [OAuth client identity](#oauth-client-identity). Defaults to `eudiw-abca`.
- **tokenRequestHeaders**: _Object_ (optional) - Additional headers to attach to the token request.
---
##### Example Response
Returns the same shape as [Request Token](#request-token) — an `accessToken`, optional `cNonce`, and `expiresIn`. Continue with [Sign Proof](#sign-proof) and [Fetch Credential](#fetch-credential).
---
## Deferred Credentials
If the issuer isn't ready to issue immediately, the receive result returns a `transaction_id` in `deferredTransactionIds` instead of a credential. Poll the deferred endpoint with that ID until the credential is ready.
**Option: CURL**
**Endpoint:** `POST /wallet/{walletId}/credentials/receive/deferred` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)
##### Example Request
```bash
curl -X POST http://localhost:7006/wallet/{walletId}/credentials/receive/deferred \
-H 'Content-Type: application/json' \
-d '{
"deferredCredentialEndpoint": "http://localhost:7005/openid4vci/credential_deferred",
"transactionId": "8xFQ5j9Q8L-3PUdnl4RWxXgB7oNc3mK2pL9vR6tY1zA",
"accessToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
}'
```
**Body Parameters**
- **deferredCredentialEndpoint**: _String_ (required) - The deferred credential endpoint from the issuer metadata.
- **transactionId**: _String_ (required) - The transaction ID returned when issuance was deferred.
- **accessToken**: _String_ (required) - The access token from the original token response.
---
##### Example Response
```json
{
"credentialIds": ["550e8400-e29b-41d4-a716-446655440000"],
"deferredTransactionIds": {}
}
```
If the issuer is still not ready, `credentialIds` comes back empty — retry later.
---
## Advanced Options
### OAuth Client Identity
Several endpoints accept a `clientId`. It is the **OAuth 2.0** `client_id` — an identifier in the OAuth 2.0 authorization protocol — the wallet presents to the **issuer's authorization server** in token and authorization requests (and it feeds client-attestation headers when those are used).
The default, `eudiw-abca`, works with issuers that don't require client registration — including the walt.id [Issuer2](https://docs.walt.id/community-stack/issuer2/getting-started.md) in its default configuration. Set `clientId` explicitly when the issuer's authorization server requires a registered or attested client identity.
### Inline Keys
The `sign-proof` step (and the [full-flow receive](https://docs.walt.id/community-stack/wallet2/credential-receiving/overview.md)) accept an inline `key` object instead of a `keyId`. An inline key takes precedence over `keyId` and the wallet's default key. Use it only when the caller manages key material outside the wallet store — most deployments should prefer `keyId` or the default key.
```json
{
"key": {
"type": "jwk",
"jwk": { "kty": "EC", "crv": "P-256", "x": "...", "y": "...", "d": "..." }
}
}
```
The `key` uses the same serialized shape as the [key import endpoint](https://docs.walt.id/community-stack/wallet2/key-management/local.md).
### Token Request Headers
`ReceiveCredentialRequest` includes a `tokenRequestHeaders` map, intended for headers such as `OAuth-Client-Attestation` and `OAuth-Client-Attestation-PoP`.
**Note:**
The token request endpoints forward `tokenRequestHeaders` to the issuer's token endpoint. If `credentialIssuer` or `credentialIssuerBaseUrl` is supplied and client attestation is configured, the wallet can also build attestation headers from issuer metadata.
---
## Endpoint Reference
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/wallet/{walletId}/credentials/receive` | POST | Full pre-authorized code flow |
| `/wallet/{walletId}/credentials/receive/resolve-offer` | POST | Resolve a credential offer |
| `/wallet/{walletId}/credentials/receive/request-token` | POST | Exchange a pre-authorized code for a token |
| `/wallet/{walletId}/credentials/receive/authorization-url` | POST | Generate an authorization-code redirect URL |
| `/wallet/{walletId}/credentials/receive/exchange-code` | POST | Exchange an authorization code for a token |
| `/wallet/{walletId}/credentials/receive/sign-proof` | POST | Sign a proof-of-possession JWT |
| `/wallet/{walletId}/credentials/receive/fetch-credential` | POST | Fetch credential(s) from the issuer |
| `/wallet/{walletId}/credentials/receive/deferred` | POST | Poll deferred credential issuance |
--------------------------------------------------------------------------------
PAGE: Overview
URL: https://docs.walt.id/community-stack/wallet2/credential-presenting/overview
RAW: https://docs.walt.id/community-stack/wallet2/credential-presenting/overview.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Credential Presenting
The Wallet API v2 presents credentials to a verifier using [OpenID4VP 1.0](https://docs.walt.id/concepts/data-exchange-protocols/openid4vp.md) with [**DCQL**](https://docs.walt.id/concepts/data-exchange-protocols/dcql.md) (Digital Credential Query Language) for credential matching. You hand the wallet a **presentation request** — typically an `openid4vp://` URL from a verifier such as [Verifier2](https://docs.walt.id/community-stack/verifier2/getting-started.md) — and the wallet finds matching credentials, builds a presentation, and submits it.
## How It Works
Presenting a credential runs through four steps:
1. **Resolve the request** — parse the verifier's authorization request and read its DCQL query.
2. **Match credentials** — find stored credentials that satisfy the query.
3. **Build the presentation** — sign the selected credential(s) with the holder key.
4. **Submit** — send the presentation to the verifier's response endpoint.
For consent UIs, the [Isolated Flow](https://docs.walt.id/community-stack/wallet2/credential-presenting/isolated-flow.md) collapses resolve + match into a single **preview** call that also returns verifier display metadata and per-claim disclosure options, then you **build-vp-token** and **send-response** (or **reject**).
## Two Ways to Present
- **Full flow** — a single call (`POST /wallet/{walletId}/credentials/present`) auto-matches credentials from the wallet's credential stores, signs, and submits. This is what the credential guides below use.
- **[Isolated Flow](https://docs.walt.id/community-stack/wallet2/credential-presenting/isolated-flow.md)** — drive each step yourself.
- **Consent UI:** preview → build-vp-token → send-response (or reject).
- **Advanced:** resolve → match → build → send.
- Also used to present credentials that aren't stored in the wallet (managed externally).
## Selective Disclosure
For SD-JWT VC and mDL, the verifier asks for claims through the `claims` array of its DCQL query. In the full `POST /credentials/present` call, the wallet discloses accordingly automatically.
In the [Isolated Flow](https://docs.walt.id/community-stack/wallet2/credential-presenting/isolated-flow.md) consent path, **preview** returns per-claim options (`required` / `selectable`). You can then pass **`selectedDisclosureOptions`** to **build-vp-token** so the holder can include or omit optional selectively disclosable claims (required claims remain disclosed).
What "only what's asked" means differs by format:
- **SD-JWT VC** — the verifier's requested paths select which **selectively-disclosable** claims are revealed. Claims marked selectively-disclosable by the issuer that the verifier does **not** request stay hidden; a claim that isn't selectively-disclosable is always present and can't be withheld. If the verifier sends no `claims` at all, only the always-present (non-SD) claims are shared.
- **mDL (mso_mdoc)** — every data element is individually disclosable, so the wallet discloses **exactly** the elements the verifier lists in `claims` (by `[namespace, element]` path) and nothing else.
```json
// A verifier asking (via DCQL) for just two claims of an SD-JWT VC
"claims": [
{ "path": ["given_name"] },
{ "path": ["birthdate"] }
]
```
## Supported Formats
- [SD-JWT VC (IETF)](https://docs.walt.id/concepts/digital-credentials/sd-jwt-vc.md)
- [W3C VC](https://docs.walt.id/concepts/digital-credentials/verifiable-credentials-w3c.md) (JWT-signed)
- [ISO 18013-5 mDL](https://docs.walt.id/concepts/digital-credentials/mdoc-mdl-iso.md)
## Start Presenting
Choose the credential type that matches your use case:
- [SD-JWT VC via OID4VP](https://docs.walt.id/community-stack/wallet2/credential-presenting/sd-jwt-vc.md)
- [W3C VC via OID4VP](https://docs.walt.id/community-stack/wallet2/credential-presenting/w3c-vc.md)
- [mDL via OID4VP](https://docs.walt.id/community-stack/wallet2/credential-presenting/mdl.md)
Need fine-grained control? See [Isolated Flow](https://docs.walt.id/community-stack/wallet2/credential-presenting/isolated-flow.md).
--------------------------------------------------------------------------------
PAGE: SD-JWT VC via OID4VP
URL: https://docs.walt.id/community-stack/wallet2/credential-presenting/sd-jwt-vc
RAW: https://docs.walt.id/community-stack/wallet2/credential-presenting/sd-jwt-vc.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Presenting SD-JWT VC Credentials via OID4VP
This guide shows you how to present an [IETF SD-JWT VC](https://docs.walt.id/concepts/digital-credentials/sd-jwt-vc.md) credential from a wallet using the walt.id Wallet API v2. You'll take a presentation request — for example one created with [Verifier2](https://docs.walt.id/community-stack/verifier2/credential-verification/sd-jwt-vc-oid4vp.md) — and satisfy it in a single call.
[**SD-JWT VC**](https://docs.walt.id/concepts/digital-credentials/sd-jwt-vc.md): An IETF credential format supporting selective disclosure — the wallet reveals only the claims the verifier asks for.
[**OID4VP**](https://docs.walt.id/concepts/data-exchange-protocols/openid4vp.md): The protocol used to deliver the presentation from the wallet to the verifier. Credential matching uses [DCQL](https://docs.walt.id/concepts/data-exchange-protocols/dcql.md).
**Note:**
This page uses the **full presentation flow** — one call that matches, signs, and submits. To preview what will be shared, let the user choose between matches, or present a credential that isn't stored in the wallet, see [Isolated Flow](https://docs.walt.id/community-stack/wallet2/credential-presenting/isolated-flow.md).
## Prerequisites
Before you begin, ensure you have:
- **Wallet API v2 running** — Follow the [Setup](https://docs.walt.id/community-stack/wallet2/setup.md) guide. The examples use `http://localhost:7006` as the base URL (Verifier2 defaults to `7004`).
- **A wallet holding an SD-JWT VC** — [Receive one first](https://docs.walt.id/community-stack/wallet2/credential-receiving/sd-jwt-vc.md); you'll need the wallet's `walletId`.
- **A presentation request** — For a full end-to-end run, create one with the [Verifier2 SD-JWT VC guide](https://docs.walt.id/community-stack/verifier2/credential-verification/sd-jwt-vc-oid4vp.md). The request is an `openid4vp://` URL.
---
## Present the Credential
Hand the verifier's request to the wallet. This single call resolves the request, matches a stored SD-JWT VC against the verifier's DCQL query, signs the presentation with the wallet's default key, and submits it.
**Option: CURL**
**Endpoint:** `POST /wallet/{walletId}/credentials/present` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)
##### Example Request
```bash
curl -X POST http://localhost:7006/wallet/{walletId}/credentials/present \
-H 'Content-Type: application/json' \
-d '{
"requestUrl": "openid4vp://authorize?client_id=x509_san_dns%3Averifier.example.org&request_uri=http%3A%2F%2Flocalhost%3A7004%2Fopenid4vc%2Frequest%2Fabc123"
}'
```
**Path Parameters**
- **walletId**: _String_ (required) - The wallet holding the credential to present. Returned when you [create a wallet](https://docs.walt.id/community-stack/wallet2/wallets.md); to look up an existing one, list your wallets with `GET /wallet`.
**Body Parameters**
- **requestUrl**: _String_ (required) - The OID4VP authorization request URL from the verifier.
- **keyId**: _String_ (optional) - Key used to sign the presentation. Defaults to the wallet's default key.
- **did**: _String_ (optional) - DID for the presenter identity. Defaults to the wallet's default DID.
---
##### Example Response
```json
{
"transmission_success": true,
"verifier_response": {
"status": "received",
"message": "Presentation received and is being processed."
}
}
```
**Response Fields**
- **transmission_success**: _Boolean_ - Whether the wallet successfully submitted the presentation to the verifier.
- **verifier_response**: _Object_ - The raw response body returned by the verifier (for a `direct_post` submission, a status/message acknowledgement as above).
- **redirect_to**: _String_ (optional) - Where to send the user next, if the verifier returned a redirect. Omitted otherwise.
- **get_url** / **form_post_html**: _String_ (optional) - Present instead for `direct_post.jwt` / form-post response modes. Omitted for a plain `direct_post`.
**Note:**
`transmission_success: true` means the wallet delivered the presentation — it doesn't by itself mean the verifier's policies passed. Confirm the outcome on the verifier side (e.g. `GET /verification-session/{sessionId}/info` → `status: SUCCESSFUL`).
🎉 You've presented an SD-JWT VC credential to the verifier. Only the selectively-disclosable claims the verifier requested were disclosed — see [Selective Disclosure](https://docs.walt.id/community-stack/wallet2/credential-presenting/overview.md#selective-disclosure) for how the verifier's DCQL `claims` drive what's shared.
---
## Doing It Step by Step
The full-flow call above matches and submits in one shot. If you're building a wallet UI and need to show the user what will be shared before they consent — or present a credential that isn't stored in the wallet — drive the isolated endpoints yourself. The consent path is **preview → build-vp-token → send-response** (or **reject**); advanced resolve/match steps are also available. See [Isolated Flow](https://docs.walt.id/community-stack/wallet2/credential-presenting/isolated-flow.md).
---
## Next Steps
- **Present other credential types** — [W3C VC](https://docs.walt.id/community-stack/wallet2/credential-presenting/w3c-vc.md) or [mDL](https://docs.walt.id/community-stack/wallet2/credential-presenting/mdl.md).
- **Preview and consent** — Use [Isolated Flow](https://docs.walt.id/community-stack/wallet2/credential-presenting/isolated-flow.md#preview-the-presentation) to show the user which credentials and claims match before sharing.
- **Receive more credentials** — [Credential Receiving](https://docs.walt.id/community-stack/wallet2/credential-receiving/overview.md).
--------------------------------------------------------------------------------
PAGE: W3C VC via OID4VP
URL: https://docs.walt.id/community-stack/wallet2/credential-presenting/w3c-vc
RAW: https://docs.walt.id/community-stack/wallet2/credential-presenting/w3c-vc.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Presenting W3C VC Credentials via OID4VP
This guide shows you how to present a [W3C Verifiable Credential](https://docs.walt.id/concepts/digital-credentials/verifiable-credentials-w3c.md) from a wallet using the walt.id Wallet API v2. You'll take a presentation request — for example one created with [Verifier2](https://docs.walt.id/community-stack/verifier2/credential-verification/vc-oid4vp.md) — and satisfy it in a single call.
[**W3C VC**](https://docs.walt.id/concepts/digital-credentials/verifiable-credentials-w3c.md): The W3C Verifiable Credentials data model, here presented as a JWT (`jwt_vc_json`).
[**OID4VP**](https://docs.walt.id/concepts/data-exchange-protocols/openid4vp.md): The protocol used to deliver the presentation from the wallet to the verifier. Credential matching uses [DCQL](https://docs.walt.id/concepts/data-exchange-protocols/dcql.md).
**Note:**
This page uses the **full presentation flow** — one call that matches, signs, and submits. To preview what will be shared, let the user choose between matches, or present a credential that isn't stored in the wallet, see [Isolated Flow](https://docs.walt.id/community-stack/wallet2/credential-presenting/isolated-flow.md).
## Prerequisites
Before you begin, ensure you have:
- **Wallet API v2 running** — Follow the [Setup](https://docs.walt.id/community-stack/wallet2/setup.md) guide. The examples use `http://localhost:7006` as the base URL (Verifier2 defaults to `7004`).
- **A wallet holding a W3C VC** — [Receive one first](https://docs.walt.id/community-stack/wallet2/credential-receiving/w3c-vc.md); you'll need the wallet's `walletId`.
- **A presentation request** — For a full end-to-end run, create one with the [Verifier2 W3C VC guide](https://docs.walt.id/community-stack/verifier2/credential-verification/vc-oid4vp.md). The request is an `openid4vp://` URL.
---
## Present the Credential
Hand the verifier's request to the wallet. This single call resolves the request, matches a stored W3C VC against the verifier's DCQL query, signs the presentation with the wallet's default key, and submits it.
**Option: CURL**
**Endpoint:** `POST /wallet/{walletId}/credentials/present` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)
##### Example Request
```bash
curl -X POST http://localhost:7006/wallet/{walletId}/credentials/present \
-H 'Content-Type: application/json' \
-d '{
"requestUrl": "openid4vp://authorize?client_id=x509_san_dns%3Averifier.example.org&request_uri=http%3A%2F%2Flocalhost%3A7004%2Fopenid4vc%2Frequest%2Fabc123"
}'
```
**Path Parameters**
- **walletId**: _String_ (required) - The wallet holding the credential to present. Returned when you [create a wallet](https://docs.walt.id/community-stack/wallet2/wallets.md); to look up an existing one, list your wallets with `GET /wallet`.
**Body Parameters**
- **requestUrl**: _String_ (required) - The OID4VP authorization request URL from the verifier.
- **keyId**: _String_ (optional) - Key used to sign the presentation. Defaults to the wallet's default key.
- **did**: _String_ (optional) - DID for the presenter identity. Defaults to the wallet's default DID.
---
##### Example Response
```json
{
"transmission_success": true,
"verifier_response": {
"status": "received",
"message": "Presentation received and is being processed."
}
}
```
**Response Fields**
- **transmission_success**: _Boolean_ - Whether the wallet successfully submitted the presentation to the verifier.
- **verifier_response**: _Object_ - The raw response body returned by the verifier (for a `direct_post` submission, a status/message acknowledgement as above).
- **redirect_to**: _String_ (optional) - Where to send the user next, if the verifier returned a redirect. Omitted otherwise.
- **get_url** / **form_post_html**: _String_ (optional) - Present instead for `direct_post.jwt` / form-post response modes.
**Note:**
`transmission_success: true` means the wallet delivered the presentation — confirm the verification outcome on the verifier side (`GET /verification-session/{sessionId}/info` → `status: SUCCESSFUL`).
🎉 You've presented a W3C VC credential to the verifier.
---
## Doing It Step by Step
The full-flow call above matches and submits in one shot. If you're building a wallet UI and need to show the user what will be shared before they consent — or present a credential that isn't stored in the wallet — drive the isolated endpoints yourself. The consent path is **preview → build-vp-token → send-response** (or **reject**); advanced resolve/match steps are also available. See [Isolated Flow](https://docs.walt.id/community-stack/wallet2/credential-presenting/isolated-flow.md).
---
## Next Steps
- **Present other credential types** — [SD-JWT VC](https://docs.walt.id/community-stack/wallet2/credential-presenting/sd-jwt-vc.md) or [mDL](https://docs.walt.id/community-stack/wallet2/credential-presenting/mdl.md).
- **Preview and consent** — Use [Isolated Flow](https://docs.walt.id/community-stack/wallet2/credential-presenting/isolated-flow.md#preview-the-presentation) to show the user which credentials and claims match before sharing.
- **Receive more credentials** — [Credential Receiving](https://docs.walt.id/community-stack/wallet2/credential-receiving/overview.md).
--------------------------------------------------------------------------------
PAGE: mDL via OID4VP
URL: https://docs.walt.id/community-stack/wallet2/credential-presenting/mdl
RAW: https://docs.walt.id/community-stack/wallet2/credential-presenting/mdl.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Presenting mDL Credentials via OID4VP
This guide shows you how to present an [ISO 18013-5 mobile driving licence (mDL)](https://docs.walt.id/concepts/digital-credentials/mdoc-mdl-iso.md) from a wallet using the walt.id Wallet API v2. You'll take a presentation request — for example one created with [Verifier2](https://docs.walt.id/community-stack/verifier2/credential-verification/mdl-oid4vp.md) — and satisfy it in a single call.
[**mDL / mdoc**](https://docs.walt.id/concepts/digital-credentials/mdoc-mdl-iso.md): The ISO/IEC 18013-5 mobile document format (`mso_mdoc`).
[**OID4VP**](https://docs.walt.id/concepts/data-exchange-protocols/openid4vp.md): The protocol used to deliver the presentation from the wallet to the verifier. Credential matching uses [DCQL](https://docs.walt.id/concepts/data-exchange-protocols/dcql.md).
**Note:**
This page uses the **full presentation flow** — one call that matches, signs, and submits. To preview what will be shared, let the user choose between matches, or present a credential that isn't stored in the wallet, see [Isolated Flow](https://docs.walt.id/community-stack/wallet2/credential-presenting/isolated-flow.md).
## Prerequisites
Before you begin, ensure you have:
- **Wallet API v2 running** — Follow the [Setup](https://docs.walt.id/community-stack/wallet2/setup.md) guide. The examples use `http://localhost:7006` as the base URL (Verifier2 defaults to `7004`).
- **A wallet holding an mDL** — [Receive one first](https://docs.walt.id/community-stack/wallet2/credential-receiving/mdl.md); you'll need the wallet's `walletId`.
- **A presentation request** — For a full end-to-end run, create one with the [Verifier2 mDL guide](https://docs.walt.id/community-stack/verifier2/credential-verification/mdl-oid4vp.md). The request is an `openid4vp://` URL.
---
## Present the Credential
Hand the verifier's request to the wallet. This single call resolves the request, matches a stored mDL against the verifier's DCQL query, signs the presentation with a key, and submits it.
**Option: CURL**
**Endpoint:** `POST /wallet/{walletId}/credentials/present` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)
##### Example Request
```bash
curl -X POST http://localhost:7006/wallet/{walletId}/credentials/present \
-H 'Content-Type: application/json' \
-d '{
"requestUrl": "openid4vp://authorize?client_id=x509_san_dns%3Averifier.example.org&request_uri=http%3A%2F%2Flocalhost%3A7004%2Fopenid4vc%2Frequest%2Fabc123"
}'
```
**Path Parameters**
- **walletId**: _String_ (required) - The wallet holding the credential to present. Returned when you [create a wallet](https://docs.walt.id/community-stack/wallet2/wallets.md); to look up an existing one, list your wallets with `GET /wallet`.
**Body Parameters**
- **requestUrl**: _String_ (required) - The OID4VP authorization request URL from the verifier.
- **keyId**: _String_ (optional) - Key used to sign the presentation. Defaults to the wallet's default key. An mdoc is bound to a device key, so no DID is involved.
---
##### Example Response
```json
{
"transmission_success": true,
"verifier_response": {
"status": "received",
"message": "Presentation received and is being processed."
}
}
```
**Response Fields**
- **transmission_success**: _Boolean_ - Whether the wallet successfully submitted the presentation to the verifier.
- **verifier_response**: _Object_ - The raw response body returned by the verifier (for a `direct_post` submission, a status/message acknowledgement as above).
- **redirect_to**: _String_ (optional) - Where to send the user next, if the verifier returned a redirect. Omitted otherwise.
- **get_url** / **form_post_html**: _String_ (optional) - Present instead for `direct_post.jwt` / form-post response modes.
**Note:**
`transmission_success: true` means the wallet delivered the presentation — confirm the verification outcome on the verifier side (`GET /verification-session/{sessionId}/info` → `status: SUCCESSFUL`).
🎉 You've presented an mDL credential to the verifier. Only the data elements the verifier requested were disclosed — see [Selective Disclosure](https://docs.walt.id/community-stack/wallet2/credential-presenting/overview.md#selective-disclosure) for how the verifier's DCQL `claims` drive what's shared.
---
## Doing It Step by Step
The full-flow call above matches and submits in one shot. If you're building a wallet UI and need to show the user what will be shared before they consent — or present a credential that isn't stored in the wallet — drive the isolated endpoints yourself. The consent path is **preview → build-vp-token → send-response** (or **reject**); advanced resolve/match steps are also available. See [Isolated Flow](https://docs.walt.id/community-stack/wallet2/credential-presenting/isolated-flow.md).
---
## Next Steps
- **Present other credential types** — [SD-JWT VC](https://docs.walt.id/community-stack/wallet2/credential-presenting/sd-jwt-vc.md) or [W3C VC](https://docs.walt.id/community-stack/wallet2/credential-presenting/w3c-vc.md).
- **Preview and consent** — Use [Isolated Flow](https://docs.walt.id/community-stack/wallet2/credential-presenting/isolated-flow.md#preview-the-presentation) to show the user which credentials and claims match before sharing.
- **Receive more credentials** — [Credential Receiving](https://docs.walt.id/community-stack/wallet2/credential-receiving/overview.md).
--------------------------------------------------------------------------------
PAGE: Isolated Flow
URL: https://docs.walt.id/community-stack/wallet2/credential-presenting/isolated-flow
RAW: https://docs.walt.id/community-stack/wallet2/credential-presenting/isolated-flow.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Isolated Flow
The [full-flow endpoint](https://docs.walt.id/community-stack/wallet2/credential-presenting/overview.md) matches and submits a presentation in a single call. When you're building a wallet UI and need to show the user what will be shared, let them choose credentials and claim disclosures, or present a credential that isn't stored in the wallet, drive the **isolated endpoints** shown on this page yourself.
For holder consent UIs, start with **preview**: one call resolves and validates the request, matches wallet credentials, and returns verifier metadata plus selectable credentials and claim options. After the user chooses, call **build-vp-token**, then **send-response**. To decline, call **reject** with the same `requestUrl`. Resolve and match remain available when you already have an authorization request or credentials outside the wallet.
All endpoints on this page are rooted at `/wallet/{walletId}/credentials/present`. The examples use `http://localhost:7006` as the base URL (Verifier2 defaults to `7004`).
## Prerequisites
Before you begin, ensure you have:
- **Wallet API v2 running** — Follow the [Setup](https://docs.walt.id/community-stack/wallet2/setup.md) guide.
- **A wallet holding credentials** — Receive credentials first, or prepare inline credentials in the wallet's JSON representation.
- **A presentation request** — Use an `openid4vp://` URL or inline request object from a verifier such as [Verifier2](https://docs.walt.id/community-stack/verifier2/getting-started.md).
---
## When to Use the Isolated Flow
Generally speaking, you should use the isolated flow when building any form of wallet user interface leveraging the wallet API v2. This allows you to properly step through the presentation process and build the necessary UI elements accordingly.
| You want to… | Use |
|---|---|
| Present in one call | [Full flow](https://docs.walt.id/community-stack/wallet2/credential-presenting/overview.md) |
| Show a consent preview (verifier + matches + claims) | [Preview the presentation](#preview-the-presentation) |
| Build a VP token from selected credentials / disclosures | [Build the VP Token](#build-the-vp-token) |
| Submit a manually built response | [Send the Response](#send-the-response) |
| Decline the presentation request | [Reject the presentation](#reject-the-presentation) |
| Read what the verifier is asking for (advanced) | [Resolve the request](#advanced-resolve-the-request) |
| DCQL-match without the full consent preview (advanced) | [Match from the wallet stores](#match-from-the-wallet-stores) |
| Present a credential not held by the wallet | [Present inline credentials](#present-inline-credentials) |
### Consent UI flow
```text
1. Preview → 2. Build VP Token → 3. Send Response
↘ Reject (with the same requestUrl)
```
Preview is **stateless**: the wallet does not retain a preview handle. Keep the original `requestUrl` and pass it to **build-vp-token**, **send-response**, and **reject**. The wallet re-resolves and revalidates that URL on each sensitive step. The preview `authorizationRequest` is for display / technical details only.
Any verifier trust configuration you've supplied to the wallet — X.509 trust anchors, verifier-attestation issuers, and pre-registered clients — is applied consistently across preview, build-vp-token, send-response, and reject, so the isolated flow validates the verifier the same way the full flow does.
---
## Preview the Presentation
Resolve and validate the OpenID4VP request, match the wallet's stored credentials, and return everything a consent UI needs — verifier display metadata, matched credential options, per-claim selective-disclosure flags, and any credential-set requirements.
**Option: CURL**
**Endpoint:** `POST /wallet/{walletId}/credentials/present/preview` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)
##### Example Request
```bash
curl -X POST http://localhost:7006/wallet/{walletId}/credentials/present/preview \
-H 'Content-Type: application/json' \
-d '{
"requestUrl": "openid4vp://authorize?client_id=x509_san_dns%3Averifier.example.org&request_uri=http%3A%2F%2Flocalhost%3A7004%2Fopenid4vc%2Frequest%2Fabc123"
}'
```
**Path Parameters**
- **walletId**: _String_ (required) - The wallet handling the presentation. See [how to obtain it](https://docs.walt.id/community-stack/wallet2/wallets.md).
**Body Parameters**
- **requestUrl**: _String_ (required) - The OID4VP authorization request URL.
- **keyId**: _String_ (optional) - Signing key used for wallet capability advertisement and request validation during preview. Defaults to the wallet's default signing key. The Ready response echoes this back as `keyId` — pass the same value to [Build the VP Token](#build-the-vp-token) so preview and build agree on key capabilities (e.g. supported algorithms).
---
##### Example Response
```json
{
"authorizationRequest": {
"nonce": "n-0S6_WzA2Mj",
"client_id": "x509_san_dns:verifier.example.org",
"response_uri": "http://localhost:7004/openid4vc/response",
"dcql_query": {
"credentials": [
{
"id": "identity_credential",
"format": "dc+sd-jwt",
"claims": [
{ "path": ["given_name"] },
{ "path": ["family_name"] }
]
}
]
}
},
"valid": true,
"keyId": "6ba7b810-9dad-11d1-80b4-preview-key",
"clientId": "x509_san_dns:verifier.example.org",
"verifier": {
"name": "Example Verifier",
"locale": "en-US",
"logoUri": "https://verifier.example.org/logo.png",
"clientUri": "https://verifier.example.org",
"policyUri": null,
"termsOfServiceUri": null
},
"responseUri": "http://localhost:7004/openid4vc/response",
"state": "af0ifjsldkj",
"nonce": "n-0S6_WzA2Mj",
"responseEncryption": {
"required": false
},
"transactionData": [],
"credentialOptions": [
{
"queryId": "identity_credential",
"credentialId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"multiple": false,
"format": "dc+sd-jwt",
"issuer": "http://localhost:7005/openid4vci",
"subject": "did:key:z6Mk…",
"label": null,
"credentialData": {
"given_name": "Ada",
"family_name": "Lovelace",
"vct": "https://issuer.example.com/identity_credential"
},
"disclosures": [
{
"path": "given_name",
"name": "given_name",
"value": "Ada",
"selectivelyDisclosable": true,
"required": true,
"selectable": false
},
{
"path": "family_name",
"name": "family_name",
"value": "Lovelace",
"selectivelyDisclosable": true,
"required": false,
"selectable": true
}
]
}
],
"credentialRequirements": [],
"error": null
}
```
**Response Fields**
- **authorizationRequest**: _Object_ - Resolved authorization request for display / technical details only.
- **valid**: _Boolean_ - Whether the request passed wallet validation. When `false`, inspect **error** and optionally [reject](#reject-the-presentation) with that error.
- **keyId**: _String_ (Ready only) - Signing key the preview used for wallet capability advertisement and validation. Pass this back as `keyId` to [Build the VP Token](#build-the-vp-token) so preview and build stay consistent.
- **clientId**: _String_ - The verifier's client identifier.
- **verifier**: _Object_ (optional) - Localized verifier display metadata from `client_metadata` (`name`, `locale`, `logoUri`, `clientUri`, `policyUri`, `termsOfServiceUri`). Supplied by the verifier — not evidence of trust on its own.
- **responseUri** / **state** / **nonce**: Request metadata projected for the UI.
- **responseEncryption**: _Object_ - Whether response encryption is required and related algorithms/keys when present.
- **transactionData**: _Array_ - Decoded `transaction_data` items attached to the request (`type`, `credentialQueryIds`, `rawJson`, `details`). Types must be registered in [`transaction-data-profiles.conf`](https://docs.walt.id/community-stack/wallet2/configurations/config-files/transaction-data-profiles.md); discover them via `GET /transaction-data-profiles`.
- **credentialOptions**: _Array_ - Wallet credentials that satisfy a DCQL credential query. Each entry includes:
- **queryId** / **credentialId** — DCQL query id and wallet credential id (use these in build selections).
- **multiple** / **format** / **issuer** / **subject** / **label** / **credentialData** — display and matching context.
- **disclosures** — claims that may be shared (`path`, `name`, `value`, `selectivelyDisclosable`, `required`, `selectable`).
- **credentialRequirements**: _Array_ - When DCQL credential sets apply: each item has `options` (a list of query-id combinations); at least one combination must be satisfied.
- **error**: _Object_ (optional) - When `valid` is `false`: `code` and optional `message`.
---
## Build the VP Token
Build and sign a `vp_token` from the credentials (and optional claim disclosures) the user selected after preview or match. The wallet re-resolves and revalidates `requestUrl` first, then uses that fresh authorization request for DCQL matching and signing. This step does not send anything to the verifier.
**Option: CURL**
**Endpoint:** `POST /wallet/{walletId}/credentials/present/build-vp-token` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)
##### Example Request (after preview)
```bash
curl -X POST http://localhost:7006/wallet/{walletId}/credentials/present/build-vp-token \
-H 'Content-Type: application/json' \
-d '{
"requestUrl": "openid4vp://authorize?client_id=x509_san_dns%3Averifier.example.org&request_uri=http%3A%2F%2Flocalhost%3A7004%2Fopenid4vc%2Frequest%2Fabc123",
"selectedCredentialOptions": [
{
"queryId": "identity_credential",
"credentialId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
}
],
"selectedDisclosureOptions": [
{
"queryId": "identity_credential",
"credentialId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"path": "family_name"
}
]
}'
```
##### Example Request (legacy IDs after match)
```bash
curl -X POST http://localhost:7006/wallet/{walletId}/credentials/present/build-vp-token \
-H 'Content-Type: application/json' \
-d '{
"requestUrl": "openid4vp://authorize?client_id=x509_san_dns%3Averifier.example.org&request_uri=http%3A%2F%2Flocalhost%3A7004%2Fopenid4vc%2Frequest%2Fabc123",
"selectedCredentialIds": {
"identity_credential": ["6ba7b810-9dad-11d1-80b4-00c04fd430c8"]
}
}'
```
**Path Parameters**
- **walletId**: _String_ (required) - The wallet holding the selected credentials.
**Body Parameters**
- **requestUrl**: _String_ (required) - The same OID4VP authorization request URL used for [Preview](#preview-the-presentation) (or [Resolve the Request](#advanced-resolve-the-request)). Re-resolved and revalidated on every call.
- **selectedCredentialOptions**: _Array_ (optional) - Preferred for consent UIs. List of `{ queryId, credentialId }` from `credentialOptions`. When non-empty, takes precedence over `selectedCredentialIds`.
- **selectedDisclosureOptions**: _Array_ (optional) - Selectively disclosable claim paths to include: `{ queryId, credentialId, path }` (paths from that option's `disclosures`). Omit to keep the default disclosure set for the selection (required claims plus matcher defaults). When provided as an empty list, optional selectively disclosable claims are not added beyond what the protocol still requires.
- **selectedCredentialIds**: _Object_ (optional) - Legacy map of DCQL query ID to wallet credential IDs (from [Match from the wallet stores](#match-from-the-wallet-stores)). Used when `selectedCredentialOptions` is empty.
- **key**: _Object_ (optional) - Inline key used to sign the VP token. Takes precedence over `keyId`.
- **keyId**: _String_ (optional) - Key used to sign the VP token. Defaults to the wallet's default key. When you drove the flow through [Preview](#preview-the-presentation) with a specific `keyId`, pass the same value (returned as `keyId` in the Ready response) here so preview and build use the same signing key.
- **did**: _String_ (optional) - DID for holder binding. Defaults to the wallet's default DID.
---
##### Example Response
```json
{
"vpToken": "{\"identity_credential\":\"eyJhbGciOiJFUzI1NiIs...\"}",
"idToken": null
}
```
**Response Fields**
- **vpToken**: _String_ - Serialized `vp_token` JSON string.
- **idToken**: _String_ (optional) - Self-issued ID token for SIOPv2 flows, when one is required.
---
## Send the Response
Send the built VP token to the verifier. The wallet re-resolves and revalidates `requestUrl` first, then transmits according to the response mode / URI / encryption from that freshly resolved request.
**Option: CURL**
**Endpoint:** `POST /wallet/{walletId}/credentials/present/send-response` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)
##### Example Request
```bash
curl -X POST http://localhost:7006/wallet/{walletId}/credentials/present/send-response \
-H 'Content-Type: application/json' \
-d '{
"requestUrl": "openid4vp://authorize?client_id=x509_san_dns%3Averifier.example.org&request_uri=http%3A%2F%2Flocalhost%3A7004%2Fopenid4vc%2Frequest%2Fabc123",
"vpToken": "{\"identity_credential\":\"eyJhbGciOiJFUzI1NiIs...\"}",
"idToken": null
}'
```
**Body Parameters**
- **requestUrl**: _String_ (required) - The same OID4VP authorization request URL used for [Preview](#preview-the-presentation). Re-resolved and revalidated on every call.
- **vpToken**: _String_ (required) - The `vpToken` returned by [Build the VP Token](#build-the-vp-token).
- **idToken**: _String_ (optional) - The `idToken` returned by [Build the VP Token](#build-the-vp-token), when present.
---
##### Example Response
Returns the same shape as the [full presentation flow](https://docs.walt.id/community-stack/wallet2/credential-presenting/overview.md) — `transmission_success`, `verifier_response`, and optionally `redirect_to`.
---
## Reject the Presentation
Decline the presentation request. The wallet re-resolves the original `requestUrl` and returns an OpenID4VP error response to the verifier when a response channel is available. No prior preview is required.
**Option: CURL**
**Endpoint:** `POST /wallet/{walletId}/credentials/present/reject` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)
##### Example Request
```bash
curl -X POST http://localhost:7006/wallet/{walletId}/credentials/present/reject \
-H 'Content-Type: application/json' \
-d '{
"requestUrl": "openid4vp://authorize?client_id=x509_san_dns%3Averifier.example.org&request_uri=http%3A%2F%2Flocalhost%3A7004%2Fopenid4vc%2Frequest%2Fabc123",
"errorCode": "access_denied",
"errorDescription": "User declined"
}'
```
**Body Parameters**
- **requestUrl**: _String_ (required) - The same OID4VP authorization request URL used for preview.
- **errorCode**: _String_ (optional) - OpenID4VP error code. Defaults to `access_denied`.
- **errorDescription**: _String_ (optional) - Human-readable description sent to the verifier.
---
##### Example Response
Returns the same `WalletPresentResult` shape as [Send the Response](#send-the-response) (for example `transmission_success` and any redirect URL produced by the reject handling).
---
## Advanced: Resolve the Request
Parse the verifier's authorization request to read who is asking and what they want, without matching credentials. Prefer [Preview](#preview-the-presentation) for consent UIs.
**Option: CURL**
**Endpoint:** `POST /wallet/{walletId}/credentials/present/resolve-request` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)
##### Example Request
```bash
curl -X POST http://localhost:7006/wallet/{walletId}/credentials/present/resolve-request \
-H 'Content-Type: application/json' \
-d '{
"requestUrl": "openid4vp://authorize?client_id=x509_san_dns%3Averifier.example.org&request_uri=http%3A%2F%2Flocalhost%3A7004%2Fopenid4vc%2Frequest%2Fabc123"
}'
```
**Path Parameters**
- **walletId**: _String_ (required) - The wallet handling the presentation. See [how to obtain it](https://docs.walt.id/community-stack/wallet2/wallets.md).
**Body Parameters**
- **requestUrl**: _String_ (required) - The OID4VP authorization request URL.
---
##### Example Response
```json
{
"authorizationRequest": {
"nonce": "n-0S6_WzA2Mj",
"client_id": "x509_san_dns:verifier.example.org",
"response_uri": "http://localhost:7004/openid4vc/response",
"dcql_query": {
"credentials": [
{
"id": "identity_credential",
"format": "dc+sd-jwt"
}
]
}
},
"nonce": "n-0S6_WzA2Mj",
"clientId": "x509_san_dns:verifier.example.org",
"responseUri": "http://localhost:7004/openid4vc/response",
"hasRequestUri": true,
"dcqlQuery": {
"credentials": [
{
"id": "identity_credential",
"format": "dc+sd-jwt"
}
]
}
}
```
**Response Fields**
- **authorizationRequest**: _Object_ - Resolved authorization request for inspection / display.
- **nonce**: _String_ - The verifier's nonce, which the presentation must be bound to.
- **clientId**: _String_ - The verifier's client identifier.
- **responseUri**: _String_ - Where the presentation will be submitted.
- **hasRequestUri**: _Boolean_ - Whether the request was fetched from a `request_uri` (vs supplied inline).
- **dcqlQuery**: _Object_ - The verifier's DCQL query, ready to pass to [Match from the Wallet Stores](#match-from-the-wallet-stores) or [Match Inline Credentials](#match-inline-credentials).
---
## Match from the Wallet Stores
Run the `dcqlQuery` returned by [Resolve the Request](#advanced-resolve-the-request) against the wallet's stored credentials to see what would satisfy it — without submitting anything. For a full consent preview (verifier display + claim options), use [Preview](#preview-the-presentation) instead.
**Option: CURL**
**Endpoint:** `POST /wallet/{walletId}/credentials/present/match-credentials-from-store` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)
##### Example Request
```bash
curl -X POST http://localhost:7006/wallet/{walletId}/credentials/present/match-credentials-from-store \
-H 'Content-Type: application/json' \
-d '{
"dcqlQuery": {
"credentials": [
{
"id": "identity_credential",
"format": "dc+sd-jwt",
"meta": { "vct_values": ["https://issuer.example.com/identity_credential"] },
"claims": [
{ "path": ["given_name"] },
{ "path": ["family_name"] }
]
}
]
}
}'
```
**Path Parameters**
- **walletId**: _String_ (required) - The wallet whose stored credentials are matched.
**Body Parameters**
- **dcqlQuery**: _Object_ (required) - The verifier's DCQL query returned by [Resolve the Request](#advanced-resolve-the-request).
---
##### Example Response
```json
{
"matchedQueryIds": ["credential_1"],
"matchCount": 2,
"matchedCredentialIds": {
"credential_1": ["6ba7b810-9dad-11d1-80b4-00c04fd430c8"]
}
}
```
**Response Fields**
- **matchedQueryIds**: _String[]_ - The DCQL credential-query IDs that were satisfied.
- **matchCount**: _Number_ - Total number of matching credentials.
- **matchedCredentialIds**: _Object_ - Map of DCQL query ID to wallet-assigned credential IDs. Use these IDs with [Build the VP Token](#build-the-vp-token) via `selectedCredentialIds`.
After the user consents, complete the manual presentation with [Build the VP Token](#build-the-vp-token) and [Send the Response](#send-the-response), or use the [full-flow endpoint](https://docs.walt.id/community-stack/wallet2/credential-presenting/overview.md) to match and submit in one call.
## Match Inline Credentials
Run a DCQL query against credentials you supply in the request, rather than the wallet's credential stores. Useful for testing a query, or matching credentials held outside the wallet.
**Option: CURL**
**Endpoint:** `POST /wallet/{walletId}/credentials/present/match-credentials` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)
##### Example Request
```bash
curl -X POST http://localhost:7006/wallet/{walletId}/credentials/present/match-credentials \
-H 'Content-Type: application/json' \
-d '{
"dcqlQuery": { "credentials": [ { "id": "credential_1", "format": "dc+sd-jwt", "meta": { "vct_values": ["https://issuer.example.com/identity_credential"] }, "claims": [ { "path": ["given_name"] } ] } ] },
"credentials": [
{ "id": "inline-1", "credential": { "type": "vc-sd_jwt", "dmtype": "sdjwtvcdm", "signedWithDisclosures": "eyJ...~WyJ...~", "credentialData": { "vct": "https://issuer.example.com/identity_credential", "given_name": "John" } } }
]
}'
```
**Body Parameters**
- **dcqlQuery**: _Object_ (required) - The DCQL query to match against.
- **credentials**: _Array_ (required) - The credentials to match. Each entry has an `id` (your own label, echoed back in the result) and a `credential`: the credential in the wallet's own JSON representation — **not** a raw credential string. Its `type` field marks the format (`vc-sd_jwt`, `vc-w3c_2`, or `mso_mdoc`); the shape beyond that is internal to each format (for `vc-sd_jwt`: `dmtype`, `signedWithDisclosures`, `credentialData`, and — only when the credential has selectively disclosable claims — `disclosables`, `disclosures`, and `originalCredentialData`). Don't hand-construct this object — the simplest and most reliable way to get one in the right shape is to copy the `credential` object from a [`GET /wallet/{walletId}/credentials/{id}`](https://docs.walt.id/community-stack/wallet2/credential-management.md) response.
---
##### Example Response
Returns the same `MatchCredentialsResult` shape as [Match from the wallet stores](#match-from-the-wallet-stores).
---
## Present Inline Credentials
Present a credential that isn't stored in the wallet — you supply it directly in the request. The wallet resolves the request, builds and signs the presentation from the inline credential, and submits it.
**Option: CURL**
**Endpoint:** `POST /wallet/{walletId}/credentials/present/isolated` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)
##### Example Request
```bash
curl -X POST http://localhost:7006/wallet/{walletId}/credentials/present/isolated \
-H 'Content-Type: application/json' \
-d '{
"requestUrl": "openid4vp://authorize?client_id=verifier2&request_uri=http%3A%2F%2Flocalhost%3A7004%2Fverification-session%2Fabc123%2Frequest",
"credentials": [
{ "id": "inline-1", "credential": { "type": "vc-sd_jwt", "dmtype": "sdjwtvcdm", "signedWithDisclosures": "eyJ...~WyJ...~", "credentialData": { "vct": "https://issuer.example.com/identity_credential", "given_name": "John" } } }
],
"keyId": "my-holder-key"
}'
```
**Body Parameters**
- **requestUrl**: _String_ (required) - The OID4VP authorization request URL.
- **credentials**: _Array_ (required) - The credentials to present. Each entry has an `id` and a `credential`: the credential in the wallet's own JSON representation (same field structure as [Match Inline Credentials](#match-inline-credentials) above), **not** a raw credential string. Unlike matching, actually building and signing a presentation needs the **complete** object — copy it as-is from a [`GET /wallet/{walletId}/credentials/{id}`](https://docs.walt.id/community-stack/wallet2/credential-management.md) response rather than trimming fields; for an SD-JWT VC with selectively disclosable claims this includes `signature`, `signed`, `disclosables`, `disclosures`, and `originalCredentialData` alongside the fields shown above.
- **keyId**: _String_ (optional) - Key used to sign the presentation. Must be the holder key the inline credential is bound to. Defaults to the wallet's default key.
- **did**: _String_ (optional) - DID for the presenter identity. Defaults to the wallet's default DID.
---
##### Example Response
Returns the same shape as the [full presentation flow](https://docs.walt.id/community-stack/wallet2/credential-presenting/overview.md) — `transmission_success`, `verifier_response`, and optionally `redirect_to`.
---
## Endpoint Reference
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/wallet/{walletId}/credentials/present` | POST | Full presentation flow (match from stores, sign, submit) |
| `/wallet/{walletId}/credentials/present/preview` | POST | Consent preview: resolve, validate, match, claim options |
| `/wallet/{walletId}/credentials/present/build-vp-token` | POST | Re-resolve `requestUrl`, then build and sign a VP token from selected stored credentials |
| `/wallet/{walletId}/credentials/present/send-response` | POST | Re-resolve `requestUrl`, then send a manually built VP response to the verifier |
| `/wallet/{walletId}/credentials/present/reject` | POST | Reject a presentation request (re-resolve `requestUrl`) |
| `/wallet/{walletId}/credentials/present/isolated` | POST | Present inline credentials |
| `/wallet/{walletId}/credentials/present/resolve-request` | POST | Parse a VP authorization request (advanced) |
| `/wallet/{walletId}/credentials/present/match-credentials-from-store` | POST | DCQL-match the wallet's stored credentials (advanced) |
| `/wallet/{walletId}/credentials/present/match-credentials` | POST | DCQL-match inline credentials |
--------------------------------------------------------------------------------
PAGE: Credential Management
URL: https://docs.walt.id/community-stack/wallet2/credential-management
RAW: https://docs.walt.id/community-stack/wallet2/credential-management.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Credential Management
Once a wallet has [received](https://docs.walt.id/community-stack/wallet2/credential-receiving/overview.md) credentials, you can list them, inspect a single credential in full, import a raw credential directly, and delete credentials.
**Note:**
**Local Development:** the API is available at `http://localhost:7006` by default, whether Wallet API v2 runs standalone or via the Docker Compose quick start.
---
## List Credentials
List the metadata of every credential in the wallet. This is lightweight — it does **not** include the raw credential data.
**Option: CURL**
**Endpoint:** `GET /wallet/{walletId}/credentials` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)
##### Example Request
```bash
curl http://localhost:7006/wallet/{walletId}/credentials
```
**Path Parameters**
- **walletId**: _String_ (required) - The wallet whose credentials you want to list. See [Managing Wallets](https://docs.walt.id/community-stack/wallet2/wallets.md).
---
##### Example Response
```json
[
{
"id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"format": "dc+sd-jwt",
"issuer": "http://localhost:7005/openid4vci",
"subject": "did:key:z6Mkfriq1MqLBoPWecGoDLjguo1sB9brj6wT3qZ5BxkKpuP6",
"label": null,
"addedAt": "2026-07-08T10:15:30Z"
}
]
```
**Response Fields**
- **id**: _String_ - The credential's ID, used to fetch or delete it.
- **format**: _String_ - Credential format (`dc+sd-jwt`, `jwt_vc_json`, `mso_mdoc`).
- **issuer** / **subject**: _String_ - Issuer and holder identifiers, when available.
- **label**: _String_ - Optional label set on import.
- **addedAt**: _String_ - ISO-8601 timestamp of when the credential was stored.
---
## Get a Credential
Fetch a single credential, including its parsed contents and raw data.
**Option: CURL**
**Endpoint:** `GET /wallet/{walletId}/credentials/{credentialId}` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)
##### Example Request
```bash
curl http://localhost:7006/wallet/{walletId}/credentials/{credentialId}
```
**Path Parameters**
- **walletId**: _String_ (required) - The wallet holding the credential.
- **credentialId**: _String_ (required) - The credential to fetch. Obtain it from [list credentials](#list-credentials).
---
##### Example Response
```json
{
"id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"credential": {
"format": "dc+sd-jwt",
"...": "parsed credential contents"
},
"label": null,
"addedAt": "2026-07-08T10:15:30Z",
"metadata": {
"issuerDisplay": [
{
"name": "Example Issuer",
"locale": "en-US",
"logo": {
"uri": "https://issuer.example.com/logo.png",
"alt_text": "Example Issuer"
}
}
]
}
}
```
**Response Fields**
- **id**: _String_ - The credential's ID.
- **credential**: _Object_ - The parsed credential, including its raw representation.
- **label** / **addedAt**: The optional label and storage timestamp.
- **metadata**: _Object_ (optional) - Sidecar JSON stored with the credential. When the credential was received via [OID4VCI](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md), this may include **`issuerDisplay`** — an array of issuer display entries (`name`, `locale`, optional `logo` with `uri` / `alt_text`) captured from issuer metadata at receive time.
---
## Import a Credential
Store a raw credential directly — for example one obtained outside the wallet, or fetched via the [Isolated Flow](https://docs.walt.id/community-stack/wallet2/credential-receiving/isolated-flow.md#fetch-credential) fetch step with `storeInWallet` left as `false`.
**Option: CURL**
**Endpoint:** `POST /wallet/{walletId}/credentials/import` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)
##### Example Request
```bash
curl -X POST http://localhost:7006/wallet/{walletId}/credentials/import \
-H 'Content-Type: application/json' \
-d '{
"rawCredential": "eyJhbGciOiJFUzI1NiIsInR5cCI6InZjK3NkLWp3dCJ9...",
"label": "My identity credential"
}'
```
**Path Parameters**
- **walletId**: _String_ (required) - The wallet to import into.
**Body Parameters**
- **rawCredential**: _String_ (required) - The raw credential (JWT, SD-JWT, or base64url mdoc). The wallet detects the format automatically.
- **label**: _String_ (optional) - A human-readable label to store alongside the credential.
---
##### Example Response
```json
{
"id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"credential": { "format": "dc+sd-jwt", "...": "parsed credential contents" },
"label": "My identity credential",
"addedAt": "2026-07-08T10:15:30Z"
}
```
---
## Delete a Credential
**Option: CURL**
**Endpoint:** `DELETE /wallet/{walletId}/credentials/{credentialId}` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)
##### Example Request
```bash
curl -X DELETE http://localhost:7006/wallet/{walletId}/credentials/{credentialId}
```
**Path Parameters**
- **walletId**: _String_ (required) - The wallet holding the credential.
- **credentialId**: _String_ (required) - The credential to delete.
---
##### Example Response
`204 No Content` on success, or `404 Not Found` if the credential doesn't exist.
---
## Next Steps
- [Present a credential](https://docs.walt.id/community-stack/wallet2/credential-presenting/sd-jwt-vc.md) to a verifier.
- [Receive more credentials](https://docs.walt.id/community-stack/wallet2/credential-receiving/overview.md).
--------------------------------------------------------------------------------
PAGE: Feature Manager
URL: https://docs.walt.id/community-stack/wallet2/configurations/feature-manager
RAW: https://docs.walt.id/community-stack/wallet2/configurations/feature-manager.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Feature Manager
Use the feature manager to control which Wallet API v2 features are enabled at service startup. This reduces complexity and the risk of misconfigurations.
## Feature Types
The feature manager offers two types: **Base** and **Optional**.
### Base Features
Base features are always enabled and provide critical functionality expected to be available by default. For the Wallet API v2, the core `wallet-service` is a base feature.
### Optional Features
Optional features can be enabled or disabled based on your requirements.
1. **Enabled by default**: work without additional configuration.
2. **Disabled by default**: must be explicitly enabled in `_features.conf`, and may require their own configuration file.
**Note:**
Each feature may have zero, one, or multiple configuration files associated with it.
## How the Feature Manager Works
1. **Initialisation with Feature Catalogs**: the service is passed its feature catalog on startup.
2. **Determination of Active Features**: the initializer resolves which features are explicitly enabled or disabled, and the default state of the rest.
3. **Loading Configurations**: only the configurations for base features and explicitly enabled optional features are loaded.
**Note:**
The service must be **restarted** for feature-flag changes to take effect — there is no hot reload.
## Feature Configuration
Configure features by editing `_features.conf`, as explained in [Features](https://docs.walt.id/community-stack/wallet2/configurations/config-files/features.md).
--------------------------------------------------------------------------------
PAGE: Features
URL: https://docs.walt.id/community-stack/wallet2/configurations/config-files/features
RAW: https://docs.walt.id/community-stack/wallet2/configurations/config-files/features.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Features
`_features.conf` configures which Wallet API v2 features are enabled at service startup. The [feature manager](https://docs.walt.id/community-stack/wallet2/configurations/feature-manager.md) reads this file before loading feature-specific configuration.
**Note:**
Feature changes apply only after a service restart.
## Fields
### `enabledFeatures`
_String[]_ — Optional features to enable.
Supported values:
- **`wallet2-persistence`** — SQL persistence (SQLite or PostgreSQL) for wallets, keys, DIDs, and credentials. Loads [wallet2-persistence.conf](https://docs.walt.id/community-stack/wallet2/configurations/config-files/persistence.md). The shipped config enables it for SQLite persistence.
- **`auth`** — Email/password authentication and account-to-wallet ownership. Loads [auth.conf](https://docs.walt.id/community-stack/wallet2/configurations/config-files/auth.md). When enabled, new authenticated wallets are linked to the current account and wallet routes enforce ownership.
- **`dev-mode`** — Development mode. Loads [dev-mode.conf](https://docs.walt.id/community-stack/wallet2/configurations/config-files/dev-mode.md). This feature is currently a no-op; the service does not read it yet.
- **`transaction-data-profiles`** — [OpenID4VP](https://docs.walt.id/concepts/data-exchange-protocols/openid4vp.md) transaction-data type registry and discovery. Loads [transaction-data-profiles.conf](https://docs.walt.id/community-stack/wallet2/configurations/config-files/transaction-data-profiles.md). **Enabled by default.**
### `disabledFeatures`
_String[]_ — Features to explicitly disable.
Use this when you need to override a default-enabled optional feature (for example `transaction-data-profiles`). The standard infrastructure features (web server, OpenAPI/Swagger, and health checks) are provided by the framework and enabled by default; the web server is configured via [web.conf](https://docs.walt.id/community-stack/wallet2/configurations/config-files/web.md).
## Base Features
Base features are always enabled and do not need to be listed in `_features.conf`.
- **`wallet-service`** — Core wallet service for wallet, key, DID, and credential APIs. Loads [wallet-service.conf](https://docs.walt.id/community-stack/wallet2/configurations/config-files/wallet-service.md).
## Example Setup
```hocon
enabledFeatures = [
wallet2-persistence
# auth
# dev-mode
]
disabledFeatures = [
# ...
]
```
--------------------------------------------------------------------------------
PAGE: Wallet Service
URL: https://docs.walt.id/community-stack/wallet2/configurations/config-files/wallet-service
RAW: https://docs.walt.id/community-stack/wallet2/configurations/config-files/wallet-service.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Wallet Service Configuration
`wallet-service.conf` configures the core Wallet API v2 service. It is loaded by the `wallet-service` base feature.
**Note:**
The shipped config sets `publicBaseUrl` to `http://localhost:4000`. A dedicated Wallet API v2 frontend is planned; until then this value is largely a placeholder, as the current API does not yet build user-facing redirect flows from it.
## File Location
```
waltid-services/waltid-wallet-api2/config/wallet-service.conf
```
## Fields
### `publicBaseUrl`
_String_ — Public-facing base URL of the wallet service.
The value is used when building response and redirect URLs, such as [OID4VCI](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md)/[OID4VP](https://docs.walt.id/concepts/data-exchange-protocols/openid4vp.md) redirect URIs. It is the URL end users reach the wallet at, which is **not necessarily the same as the API's bind port** ([`webPort`](https://docs.walt.id/community-stack/wallet2/configurations/config-files/web.md)); typically it points at the user-facing wallet frontend.
```hocon
publicBaseUrl = "http://localhost:4000"
```
The value can be overridden with the `WALLET_PUBLIC_BASE_URL` environment variable.
## Example Configuration
```hocon
# wallet-service.conf
publicBaseUrl = "http://localhost:4000"
```
For production, set it to your public HTTPS URL:
```hocon
publicBaseUrl = "https://wallet.example.com"
```
--------------------------------------------------------------------------------
PAGE: Persistence
URL: https://docs.walt.id/community-stack/wallet2/configurations/config-files/persistence
RAW: https://docs.walt.id/community-stack/wallet2/configurations/config-files/persistence.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Persistence Configuration
`wallet2-persistence.conf` configures SQL persistence for Wallet API v2 wallets, keys, DIDs, and credentials.
**Note:**
The shipped configuration enables `wallet2-persistence`, so wallet data persists to SQLite by default and survives restarts. If you remove `wallet2-persistence` from [`_features.conf`](https://docs.walt.id/community-stack/wallet2/configurations/config-files/features.md), the service keeps all data in memory and loses it on restart.
## File Location
```
waltid-services/waltid-wallet-api2/config/wallet2-persistence.conf
```
## Fields
### `jdbcUrl`
_String_ — JDBC connection URL.
- **SQLite** — Defaults to `jdbc:sqlite:wallet2.db`.
- **PostgreSQL** — Supported with `jdbc:postgresql://...` URLs.
### `driverClassName`
_String_ — JDBC driver class.
- **SQLite** — Defaults to `org.sqlite.JDBC`.
- **PostgreSQL** — Use `org.postgresql.Driver`.
### `username`
_String_ — Database username.
This is only relevant for database engines that require credentials, such as PostgreSQL. Defaults to an empty string.
### `password`
_String_ — Database password.
This is only relevant for database engines that require credentials, such as PostgreSQL. Defaults to an empty string.
### `maximumPoolSize`
_Int_ — Maximum database connection pool size.
Defaults to `5`.
### `minimumIdle`
_Int_ — Minimum idle database connections.
Defaults to `1`.
## SQLite
```hocon
# wallet2-persistence.conf
jdbcUrl = "jdbc:sqlite:data/wallet2.db"
driverClassName = "org.sqlite.JDBC"
```
## PostgreSQL
```hocon
# wallet2-persistence.conf
jdbcUrl = "jdbc:postgresql://localhost:5432/wallet2"
driverClassName = "org.postgresql.Driver"
username = "wallet"
password = "secret"
maximumPoolSize = 10
minimumIdle = 1
```
**Note:**
Enabling persistence requires listing `wallet2-persistence` in [`_features.conf`](https://docs.walt.id/community-stack/wallet2/configurations/config-files/features.md), then restarting the service.
Set `jdbcUrl` (and the other options) directly in `wallet2-persistence.conf`. When running via Docker, mount your edited config directory into the container so these values take effect.
--------------------------------------------------------------------------------
PAGE: Authentication
URL: https://docs.walt.id/community-stack/wallet2/configurations/config-files/auth
RAW: https://docs.walt.id/community-stack/wallet2/configurations/config-files/auth.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Authentication Configuration
`auth.conf` configures email/password authentication for the Wallet API v2 `auth` optional feature.
By default the Wallet API v2 runs without authentication — every wallet is accessible without a token. Enable the `auth` [optional feature](https://docs.walt.id/community-stack/wallet2/configurations/config-files/features.md) to require email/password login and enforce that an account only accesses its own wallets.
**Note:**
Auth uses an in-memory account store in the Community Stack service. Accounts are lost on restart unless you provide a persistent account-store implementation. Wallet ownership mappings are stored through the wallet store.
## File Location
```
waltid-services/waltid-wallet-api2/config/auth.conf
```
## Fields
### `signingKey`
_Object_ — Serialized walt.id crypto key used to sign and verify JWT session tokens.
Use the same signing key on every replica so tokens issued by one instance can be verified by the others. Recommended key types are `secp256r1` (ES256) or `Ed25519` (EdDSA).
### `tokenExpiry`
_String_ — Session token lifetime as an ISO-8601 duration.
Defaults to `PT24H`. Examples: `PT30M`, `PT24H`, `P7D`.
```hocon
# auth.conf
signingKey = {
type = "jwk"
jwk = {
kty = "EC"
crv = "P-256"
x = "..."
y = "..."
d = "..."
}
}
tokenExpiry = "PT24H"
```
---
## Authentication Endpoints
When `auth` is enabled, the following endpoints are available. After logging in, pass the returned token as `Authorization: Bearer {token}` on every wallet request.
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/auth/register` | POST | Register a new account |
| `/auth/emailpass` | POST | Log in with email + password |
| `/auth/logout` | POST | Invalidate the current token |
| `/auth/account` | GET | Get the authenticated account (id, email, wallet IDs) |
| `/auth/account/wallets` | GET | List wallets owned by the account |
| `/auth/account/wallets/{walletId}` | POST | Link a wallet to the account |
### Register
**Option: CURL**
**Endpoint:** `POST /auth/register`
##### Example Request
```bash
curl -X POST http://localhost:7006/auth/register \
-H 'Content-Type: application/json' \
-d '{ "email": "user@example.com", "password": "secret123" }'
```
**Body Parameters**
- **email**: _String_ (required) - The account email.
- **password**: _String_ (required) - The account password.
---
##### Example Response
```json
{ "accountId": "b2c3d4e5-..." }
```
### Log In
**Option: CURL**
**Endpoint:** `POST /auth/emailpass`
##### Example Request
```bash
curl -X POST http://localhost:7006/auth/emailpass \
-H 'Content-Type: application/json' \
-d '{ "email": "user@example.com", "password": "secret123" }'
```
**Body Parameters**
- **email**: _String_ (required) - The account email.
- **password**: _String_ (required) - The account password.
---
##### Example Response
The response is a session object containing the bearer token. Use the token on subsequent requests:
```bash
curl http://localhost:7006/wallet \
-H 'Authorization: Bearer {token}'
```
**Note:**
New wallets created while authenticated are automatically linked to your account, so `GET /wallet` and `GET /auth/account/wallets` return only your own wallets.
--------------------------------------------------------------------------------
PAGE: Dev Mode
URL: https://docs.walt.id/community-stack/wallet2/configurations/config-files/dev-mode
RAW: https://docs.walt.id/community-stack/wallet2/configurations/config-files/dev-mode.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Dev Mode Configuration
`dev-mode.conf` configures the Wallet API v2 development-mode feature.
**Error:**
**Dev mode currently has no effect in the Wallet API v2.**
Dev mode is an [optional feature](https://docs.walt.id/community-stack/wallet2/configurations/config-files/features.md) intended for local development. Enable it by listing `dev-mode` in `_features.conf`; configure it in `dev-mode.conf`.
## File Location
```
waltid-services/waltid-wallet-api2/config/dev-mode.conf
```
## Fields
### `devMode`
_Boolean_ — Reserved for development conveniences.
Defaults to `false`. The field is currently unused — enabling it does not disable authentication or change service behavior.
```hocon
# dev-mode.conf
devMode = true
```
**Note:**
Dev mode is intended for local development only. Do not enable it in production.
--------------------------------------------------------------------------------
PAGE: Web
URL: https://docs.walt.id/community-stack/wallet2/configurations/config-files/web
RAW: https://docs.walt.id/community-stack/wallet2/configurations/config-files/web.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Web Configuration
`web.conf` configures the host and port that the Wallet API v2 web server binds to.
**Note:**
The shipped `web.conf` binds the standalone service to port `7006`. Under [Docker Compose](https://docs.walt.id/community-stack/home.md#quick-start), the port is set via the `WALLET_API2_PORT` environment variable, which also defaults to `7006`.
## File Location
```
waltid-services/waltid-wallet-api2/config/web.conf
```
## Fields
### `webHost`
_String_ — Network interface to bind to.
Defaults to `0.0.0.0`.
### `webPort`
_Int_ — Port the API service listens on.
The standalone service uses `7006` by default. This is the port you send API requests to and open Swagger on.
```hocon
# web.conf
webHost = "0.0.0.0"
webPort = 7006
```
**Note:**
This is separate from [`publicBaseUrl`](https://docs.walt.id/community-stack/wallet2/configurations/config-files/wallet-service.md) in `wallet-service.conf`, which is the wallet's public/front-end-facing base URL and need not use the same port.
--------------------------------------------------------------------------------
PAGE: Transaction Data Profiles
URL: https://docs.walt.id/community-stack/wallet2/configurations/config-files/transaction-data-profiles
RAW: https://docs.walt.id/community-stack/wallet2/configurations/config-files/transaction-data-profiles.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Transaction Data Profiles
`transaction-data-profiles.conf` defines the [OpenID4VP](https://docs.walt.id/concepts/data-exchange-protocols/openid4vp.md) `transaction_data` types Wallet API v2 accepts when previewing, presenting, building, or sending authorization responses.
The `transaction-data-profiles` optional feature is **enabled by default**. When enabled, the wallet:
- Loads type identifiers into its `TransactionDataTypeRegistry`
- Rejects authorization requests that carry unknown `transaction_data.type` values
- Exposes the configured profiles via discovery
## Discovery Endpoint
`GET /transaction-data-profiles`
Returns the configured `List` (`type`, `displayName`, `fields`).
Disable the feature (empty registry, no discovery route) by listing it under `disabledFeatures` in [`_features.conf`](https://docs.walt.id/community-stack/wallet2/configurations/config-files/features.md).
**Info:**
**mDoc issuance grant:** For `mso_mdoc` credentials, registering a type here is not enough for the holder to device-sign it. The credential's MSO must authorize that type via `KeyAuthorizations`, which Issuer2 sets from [`authorizedTransactionDataTypes`](https://docs.walt.id/community-stack/issuer2/configurations/config-files/issuer-profiles.md#authorized-transaction-data-types). SD-JWT VC presentations bind transaction data in the KB-JWT and do not use that field.
## File Location
```
waltid-services/waltid-wallet-api2/config/transaction-data-profiles.conf
```
Docker Compose ships the same file under `docker-compose/wallet-api2/config/`.
## Fields
### `transactionDataProfiles`
_Array of Objects_ — List of recognized transaction data types. Each profile defines:
| Field | Type | Description |
|---|---|---|
| `type` | String | **Required.** The transaction data type identifier. Used as the `type` field in transaction data entries and as the namespace for mdoc DeviceSigned binding. |
| `displayName` | String | Human-readable name for UIs and discovery. Defaults to `type` if omitted. |
| `fields` | Array | Expected top-level field names for documentation/UI. Nested payloads (e.g. SCA) can declare a container such as `["payload"]`. |
## Built-in Profiles
```editorconfig[transaction-data-profiles.conf]
transactionDataProfiles = [
{
type = "org.waltid.transaction-data.payment-authorization"
displayName = "Payment Authorization"
fields = ["merchant_name", "amount", "currency"]
},
{
type = "org.waltid.transaction-data.account-access"
displayName = "Account Access"
fields = ["account_identifier", "access_scope"]
},
{
type = "urn:eudi:sca:payment:1"
displayName = "SCA Payment"
fields = ["payload"]
},
{
type = "payment_card"
displayName = "Payment Card"
fields = ["merchant_name", "amount"]
}
]
```
Keep Wallet API v2 types aligned with Verifier2 and (for mDocs) Issuer2 `authorizedTransactionDataTypes`. See the shared [Verifier2 transaction-data profiles](https://docs.walt.id/community-stack/verifier2/configurations/config-files/transaction-data-profiles.md) guide for request examples and custom type instructions.
Runtime overlay mutations are not exposed on Community Wallet API v2. Durable types live in `transaction-data-profiles.conf`. Per-service create, replace, and delete live on [Enterprise Wallet2 and Verifier2](https://docs.walt.id/enterprise-stack/setup/configurations/config-files/transaction-data-profiles.md). Wallet API v1 remains discovery-only (`GET /transaction-data-profiles`).
Community Verifier2 session create does not apply a type allowlist. Keep Wallet2 types aligned with the verifier's HOCON so the wallet can present the types a verifier actually requests.
--------------------------------------------------------------------------------
PAGE: Migration from Wallet API v1
URL: https://docs.walt.id/community-stack/wallet2/migration
RAW: https://docs.walt.id/community-stack/wallet2/migration.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Migration from Wallet API v1
The `waltid-wallet-migration` tool migrates data from the old `waltid-wallet-api` (v1) database schema to the new `waltid-wallet-api2` schema. This guide covers the migration process, what gets migrated, and how to run it.
**Error:**
**Important:** Always back up your v1 database before running the migration. The migration tool is designed to be idempotent (running it twice won't duplicate data), but having a backup is essential.
---
## What Gets Migrated
The migration tool transfers the following data from v1 to v2:
| v1 Table | v2 Table(s) | Description |
|----------|-------------|-------------|
| `wallets` | `wallet2_wallets` + store registration tables | Wallet records with auto-created stores |
| `wallet_keys` | `wallet2_keys` | Keys (re-serialized with type inference) |
| `credentials` | `wallet2_credentials` | Credentials (re-parsed into the v2 credential format) |
| `wallet_dids` | `wallet2_dids` | DIDs with documents |
| `account_wallet_mapping` | `wallet2_account_wallets` | Account-to-wallet ownership |
### Store Creation
For each migrated wallet, the tool automatically creates:
- One key store: `wallet-{id}-keys`
- One credential store: `wallet-{id}-creds`
- One DID store: `wallet-{id}-dids`
These deterministic IDs ensure the migration is idempotent.
---
## Prerequisites
1. **Source database**: Your existing v1 wallet database (SQLite or PostgreSQL)
2. **Target database**: A new or empty v2 database (SQLite or PostgreSQL)
3. **Java 17+**: Required to run the migration tool
---
## Running the Migration
### Using Gradle
From the `waltid-identity` repository root:
**SQLite to SQLite:**
```bash
./gradlew :waltid-services:waltid-wallet-migration:run \
--args="--source jdbc:sqlite:/path/to/old/wallet.db --target jdbc:sqlite:/path/to/new/wallet2.db"
```
**PostgreSQL to PostgreSQL:**
```bash
./gradlew :waltid-services:waltid-wallet-migration:run \
--args="--source 'jdbc:postgresql://localhost:5432/walletv1?user=wallet&password=secret' --target 'jdbc:postgresql://localhost:5432/walletv2?user=wallet&password=secret'"
```
**Cross-database (SQLite to PostgreSQL):**
```bash
./gradlew :waltid-services:waltid-wallet-migration:run \
--args="--source jdbc:sqlite:/path/to/wallet.db --target 'jdbc:postgresql://localhost:5432/walletv2?user=wallet&password=secret'"
```
### Dry Run Mode
Add `--dry-run` to preview what would be migrated without writing anything:
```bash
./gradlew :waltid-services:waltid-wallet-migration:run \
--args="--source jdbc:sqlite:/path/to/wallet.db --target jdbc:sqlite:/path/to/wallet2.db --dry-run"
```
---
## Command Line Arguments
| Argument | Required | Description |
|----------|----------|-------------|
| `--source` | Yes | JDBC URL for the v1 source database |
| `--target` | Yes | JDBC URL for the v2 target database |
| `--dry-run` | No | Preview migration without writing data |
---
## Migration Output
The migration tool logs progress and provides a summary at the end:
```
INFO - Source: jdbc:sqlite:/path/to/wallet.db
INFO - Target: jdbc:sqlite:/path/to/wallet2.db
INFO - Found 5 wallets to migrate
INFO -
INFO - ═══════════════════════════════════════════
INFO - Migration complete
INFO - Wallets: 5
INFO - Keys: 12
INFO - Credentials: 28
INFO - DIDs: 5
INFO - Account mappings: 5
INFO - ═══════════════════════════════════════════
```
If any items fail to migrate, warnings are logged with details:
```
WARN - Failed to migrate credential abc123 for wallet xyz789: Invalid credential format
```
---
## Post-Migration Steps
### 1. Verify the Migration
After migration, verify the data was transferred correctly:
Point `wallet2-persistence.conf` at the migrated database (e.g. `jdbcUrl = "jdbc:sqlite:/data/wallet2.db"`), then start wallet-api2 with your config directory and the database mounted:
```bash
docker run \
-p 7006:7006 \
-v /path/to/config:/waltid-wallet-api2/config \
-v /path/to/data:/data \
waltid/wallet-api2
# List wallets
curl http://localhost:7006/wallet
```
### 2. Update Your Configuration
Update your deployment to use the new wallet-api2 service and database:
**wallet2-persistence.conf:**
```hocon
# wallet2-persistence.conf
jdbcUrl = "jdbc:sqlite:/data/wallet2.db"
driverClassName = "org.sqlite.JDBC"
```
Or for PostgreSQL:
```hocon
# wallet2-persistence.conf
jdbcUrl = "jdbc:postgresql://localhost:5432/walletv2"
driverClassName = "org.postgresql.Driver"
username = "wallet"
password = "secret"
maximumPoolSize = 10
```
See the [Persistence configuration reference](https://docs.walt.id/community-stack/wallet2/configurations/config-files/persistence.md) for all options.
### 3. Update API Calls
The v2 API has some differences from v1. Key changes:
| v1 Endpoint | v2 Endpoint | Notes |
|-------------|-------------|-------|
| `/api/wallet/{walletId}/...` | `/wallet/{walletId}/...` | Simplified path |
| Presentation Exchange | [DCQL](https://docs.walt.id/concepts/data-exchange-protocols/dcql.md) | Different query format for [OID4VP](https://docs.walt.id/concepts/data-exchange-protocols/openid4vp.md) |
| `/api/wallet/{walletId}/credentials/usePresentationRequest` | `/wallet/{walletId}/credentials/present` | New presentation endpoint |
---
## Troubleshooting
### "Key store not found" errors
The migration creates stores with deterministic IDs. If you see this error, ensure the target database is empty or the migration completed successfully.
### Credential parsing failures
Some v1 credentials may use formats not fully supported in v2. Check the warning logs for specific credential IDs and manually verify/re-import if needed.
### Database connection issues
Ensure the JDBC URLs are correct and the databases are accessible:
- SQLite: File path must exist and be writable
- PostgreSQL: User must have CREATE TABLE permissions
---
## Rollback
If you need to rollback:
1. Stop the wallet-api2 service
2. Restore your v1 database from backup
3. Start the wallet-api (v1) service pointing to the restored database
The migration does not modify the source database, so your v1 data remains intact.
--------------------------------------------------------------------------------
PAGE: Full API Reference
URL: https://docs.walt.id/community-stack/wallet2/full-api-reference
RAW: https://docs.walt.id/community-stack/wallet2/full-api-reference.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Full API Reference
The Wallet API v2 serves an interactive Swagger UI covering every endpoint — wallet, key, DID, and credential management, plus receiving and presenting.
When the service is running, open it at:
```
http://localhost:7006/swagger
```
The raw OpenAPI document is available at `http://localhost:7006/api.json`.
--------------------------------------------------------------------------------
PAGE: Getting Started
URL: https://docs.walt.id/community-stack/mobile-wallet/getting-started
RAW: https://docs.walt.id/community-stack/mobile-wallet/getting-started.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Getting Started
We provide an edge wallet SDK for Android and iOS. This SDK is built on top of the Walt.id Libraries and provides a modern way to build wallet mobile applications.
The primary SDK is a Kotlin Multiplatform module which we recommend using for your Kotlin Compose and Android applications. You can find the source code of the SDK [here](https://github.com/walt-id/waltid-identity/tree/main/waltid-libraries/protocols/waltid-openid4vc-wallet-mobile).
For iOS, we provide Swift-native types over our Kotlin Multiplatform module alongside a SKIE bridge to try and make the developer experience as seamless as possible for Swift developers. You can find the source code of the iOS SDK [here](https://github.com/walt-id/waltid-identity/tree/main/waltid-libraries/protocols/waltid-wallet-sdk-ios).
We also provide two sample applications to help you get started:
- A Kotlin Compose application, which builds to both Android and iOS. Uses the Multiplatform SDK and uses Kotlin Compose for the UI.
- A native iOS application, which builds to iOS only. Uses the iOS SDK and uses Native SwiftUI.
## Features
- **Wallet Management**
- Leverage encypted on-device data storage
- Use the secure enclave for key generation, signing and storage
- Generate and store DIDs
- Configure wallet attester to provide wallet attestations for usage in credential exchange flows
- **Credential Exchange**
- Full support for OpenID4VCI 1.0, including preauthorized code flow with or without transaction code, as well as the authorised code flow.Exposed as isolated steps to allow for custom UI.
- Full support for OpenID4VP 1.0, including Transaction Data. Exposed as isolated steps to allow for custom UI.
- Full support for Digital Credentials API on respective platforms (Android supports OpenID4VCI, OpenID4VP and 18013-7. iOS only support 18013-7)
- Full exposure of the issuer/verifier/credential metadata sets
- **Credential Management**
- View, delete and import credentials
## Distribution
| Name | Type | Link |
|------|------|------|
| Android SDK | Maven Module | [Source Code](https://github.com/walt-id/waltid-identity/tree/main/waltid-libraries/protocols/waltid-openid4vc-wallet-mobile) |
| iOS SDK | Swift Package | [Source Code](https://github.com/walt-id/waltid-identity/tree/main/waltid-libraries/protocols/waltid-wallet-sdk-ios) |
| Android Compose App | APK | [Download from Releases](https://github.com/walt-id/waltid-identity/releases) |
| iOS Compose App | TestFlight | Coming Soon |
| iOS Native App | TestFlight | Coming Soon |
## Further Documentation
Our repository hosts the most up to date documentation for building and contributing to the SDKs and example applications, which you can find [here](https://github.com/walt-id/waltid-identity/tree/main/docs/mobile-wallet-development.md).
We host the documentation for the Android and iOS SDKs based on our latest release [here for Android](https://sdk.docs.walt.id) and [here for iOS](https://sdk-ios.docs.walt.id)
## Roadmap
Short term roadmap:
- Support for OpenID4VC HAIP
- Support for 18013-5 Proximity Verification via BLE, NFC and Wifi-Aware
- Device Integrity Checks
- Key Backup and Recovery via native platform APIs
- Credential Backup and Recovery via Walt.id Enterprise API
- Export/Import functionality according to ARF 3.0
Feel free to reach out to us if you have any questions or suggestions via [Github Issues](https://github.com/walt-id/waltid-identity/issues)
We offer support contracts for using the SDK in your projects. Please reach out to us at [contact@walt.id](mailto:contact@walt.id) to discuss your needs.
--------------------------------------------------------------------------------
PAGE: Getting Started
URL: https://docs.walt.id/community-stack/issuer/getting-started
RAW: https://docs.walt.id/community-stack/issuer/getting-started.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Getting Started
Video: https://www.youtube.com/watch?v=uywefBo4XL0
The walt.id issuer API allows you to **sign and issue various types of digital credentials** ([**W3C Verifiable Credentials**](https://docs.walt.id/concepts/digital-credentials/verifiable-credentials-w3c.md), [**Selective Disclosure JWT (SD-JWT VC)**](https://docs.walt.id/concepts/digital-credentials/sd-jwt-vc.md) and [**Mobile Driving License (mDL)**](https://docs.walt.id/concepts/digital-credentials/mdoc-mdl-iso.md)) **via [**OpenID for Verifiable Credential Issuance (OID4VCI)**](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md)**. The issuer and
subject can optionally be identified by various [**Decentralized Identifier (DID)**](https://docs.walt.id/concepts/decentralised-identifiers.md) methods like did:key, did:jwk, did:web, did:cheqd and more.
Use [data functions](https://docs.walt.id/community-stack/issuer/credential-issuance/data-functions.md) to dynamically populate the credential with up-to-date information from
your backend during issuance.
**Error:**
This API is deprecated in favour of the new **Issuer2** API, which is built around the stable release of the OID4VCI 1.0 specification. It remains available for existing integrations, but we recommend using Issuer2 for new projects. [Documentation for the Issuer2 API can be found here](https://docs.walt.id/community-stack/issuer2/getting-started.md).
## Supported Standards
| | |
|:-------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **Credential Formats:** | [SD-JWT VC (IETF)](https://docs.walt.id/concepts/digital-credentials/sd-jwt-vc.md), [W3C VC (v1.1+, v2.0)](https://docs.walt.id/concepts/digital-credentials/verifiable-credentials-w3c.md), [ISO 18013-5 mDL](https://docs.walt.id/concepts/digital-credentials/mdoc-mdl-iso.md) |
| **Credential Exchange:** | [OID4VCI](https://docs.walt.id/concepts/data-exchange-protocols.md) (Draft 11/13, v1 coming soon),[ ISO/IEC 18013-7](https://docs.walt.id/concepts/data-exchange-protocols.md) |
| **Credential Status:** | [StatusList2021, Bitstring Status List, Token Status List](https://docs.walt.id/concepts/credential-lifecycle/credential-status-and-revocation.md) |
| **Signing Algorithms:** | ed25519, secp256k1, secp256r1, RSA |
## Setup
- [Open-Source](https://docs.walt.id/community-stack/issuer/setup.md) - Use the issuer API available via the [walt.id identity-repo](https://github.com/walt-id/waltid-identity.git).
- [Docker compose quick start](https://docs.walt.id/community-stack/home.md#quick-start) - Spin up the issuer API locally together with the other services.
- [Full API reference](https://docs.walt.id/community-stack/issuer/full-api-reference.md) - Explore all available endpoints via Swagger.
## Features
- [Keys / DIDs](https://docs.walt.id/community-stack/issuer/key-management/overview.md) - Generate and use keys/DIDs secured by an external KMS or your custom storage.
- [Credentials](https://docs.walt.id/community-stack/issuer/credential-issuance/overview.md) - Issue credentials via OID4VC.
- [Data Functions](https://docs.walt.id/community-stack/issuer/credential-issuance/data-functions.md) - Populate credentials with up-to-date information.
Checkout the full feature overview [here](https://docs.walt.id/community-stack/issuer/features).
## Ecosystems
- [EBSI](https://docs.walt.id/community-stack/issuer/ecosystems/ebsi/overview.md) - Issue credentials that are compliant with the European Blockchain Services Infrastructure (EBSI).
## Configurations
Use [the feature manager](https://docs.walt.id/community-stack/issuer/configurations/feature-manager.md) to enable or disable API functionality depending on your use-case.
## Guides
- [W3C VC issuance](https://docs.walt.id/community-stack/issuer/credential-issuance/vc-oid4vc.md) - Issue W3C Verifiable Credentials with JWT or SD-JWT signatures.
- [SD-JWT VC issuance](https://docs.walt.id/community-stack/issuer/credential-issuance/sd-jwt-vc-oid4vc.md) - Issue IETF SD-JWT VC credentials.
- [mDL issuance](https://docs.walt.id/community-stack/issuer/credential-issuance/mdl-oid4vc.md) - Issue ISO/IEC 18013-5 Mobile Driver's Licenses.
--------------------------------------------------------------------------------
PAGE: Overview
URL: https://docs.walt.id/community-stack/issuer/capabilities
RAW: https://docs.walt.id/community-stack/issuer/capabilities.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Overview
The Community Stack Issuer API is a stateless, lightweight REST API designed for building credential issuance solutions.
## Credential Issuance Capabilites
--------------------------------------------------------------------------------
PAGE: Issuer Identifiers
URL: https://docs.walt.id/community-stack/issuer/capabilities/did-identifiers
RAW: https://docs.walt.id/community-stack/issuer/capabilities/did-identifiers.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Issuer Identifiers
The Community Stack supports multiple identifier methods for your issuer—from [**Decentralized Identifiers (DIDs)**](https://docs.walt.id/concepts/decentralised-identifiers.md) to traditional X.509 certificates. This gives you flexibility to align with your ecosystem's trust model, regulatory requirements, and operational constraints.
**Note:**
"How is the issuer identified in credentials?"
## What's included
The Community Stack supports three main approaches to issuer identification, each with different operational characteristics.
### DID-based Identifiers
Your issuer uses a **Decentralized Identifier (DID)** as its identity. The DID resolves to a DID document containing the issuer's public keys, service endpoints, and metadata. When a verifier checks a credential, they resolve the issuer DID and verify the signature against the published keys.
**Supported DID methods** include, but are not limited to:
- **`did:key`** – Self-contained DID where the key is embedded in the identifier itself
- **`did:jwk`** – DID based on JSON Web Key format
- **`did:web`** – Web-based DID that resolves via HTTPS
- **`did:ebsi`** – EBSI-compliant DID for European ecosystem interoperability
### X.509 Certificate-based Identifiers
Your issuer uses an **X.509 certificate** issued by a trusted Certificate Authority (CA). The certificate contains the issuer's public key and identity information. Verifiers validate credentials by checking the certificate chain back to a trusted root CA.
### Direct Key References
Your issuer uses a **direct cryptographic key reference** (such as a JWK – JSON Web Key) without a broader identity framework. The key itself serves as the identifier, and trust is established through out-of-band mechanisms (pre-shared key lists, trust registries, or direct configuration).
## Trust & Verification
Identifier methods tell you *who* the issuer is. But verifiers also need to determine *whether to trust that issuer* for a given credential type.
Ecosystems establish trust through mechanisms like:
- **Trust registries and trust lists** (e.g., EBSI TAO, [**eIDAS 2.0**](https://walt.id/eidas2) trust lists)
- **Certificate Authority hierarchies** (traditional PKI trust chains)
- **Direct trust relationships** (pre-configured trusted issuers)
These trust mechanisms work independently of the identifier method you choose. A DID, certificate, or direct key can all be registered in a trust registry or validated against ecosystem policies.
## Key Management Integration
Regardless of which identifier method you choose, the underlying cryptographic keys used for signing are stored and managed through the Community Stack's [key management system](https://docs.walt.id/community-stack/issuer/key-management/overview.md).
For proof-of-concepts and testing, local key storage is available. For production deployments, external key management systems keep key material secure (Hashicorp Vault, Azure Key Vault, AWS KMS, OCI Vault, etc.). Learn more [here](https://docs.walt.id/community-stack/issuer/key-management/overview.md).
## FAQs
**Do I need a DID to issue credentials?**
No. You can use X.509 certificates or direct key references. The choice depends on your ecosystem's trust model and regulatory requirements.
**What's the difference between a DID and a key?**
A key is the cryptographic material used to sign credentials. A DID is a persistent identifier that resolves to a DID document containing keys (and other metadata). Keys can be rotated; the DID stays the same. A "direct key reference" uses the key itself as an identifier, which means rotating the key changes the identifier.
**Can I switch identifier methods later?**
Yes, but previously issued credentials will still reference the old identifier. Switching is effectively creating a new issuer identity. Plan your identifier strategy early; changes require ecosystem coordination (updating trust registries, verifier configurations, etc.).
## Get Started
- **[Set up your issuer](https://docs.walt.id/community-stack/issuer/setup.md)** – Configure your first issuer.
- **[Key management](https://docs.walt.id/community-stack/issuer/key-management/overview.md)** – Learn how to securely store and manage the cryptographic keys backing your issuer identifier.
- **[Issue your first credential](https://docs.walt.id/community-stack/issuer/credential-issuance/vc-oid4vc.md)** – See how your identifier appears in issued credentials and how wallets verify it.
--------------------------------------------------------------------------------
PAGE: Data & Identity Sourcing
URL: https://docs.walt.id/community-stack/issuer/capabilities/data-sourcing
RAW: https://docs.walt.id/community-stack/issuer/capabilities/data-sourcing.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Data & Identity Sourcing
Data & Identity Sourcing describes how the Community Stack Issuer obtains the attributes that go into a credential – for example name, date of birth, identifiers, roles, or any other claims about the subject.
The Community Stack gives you flexibility in where subject data lives and when it is fetched, allowing you to plug into existing systems (CRM, registries, IdPs, etc.) and design flows that fit your business, security, and UX requirements.
At a high level, this capability answers one question:
**Note:**
"Where do the attributes in my credential actually come from – and at what point in the issuance flow do we fetch them?"
## What's included
The Community Stack supports three complementary ways of sourcing data for credentials. You can use them individually or combine them in a single flow.
#### 1. Use your own backend as the source of truth (before offer creation)
Your backend collects all required subject attributes up front and passes them to the Community Stack Issuer API when creating a credential offer.
**This means:**
- You stay in control of where identity data lives (databases, registries, etc.).
- The Issuer API receives a complete, ready-to-sign data structure and focuses on protocol handling and signing.
- Simple, straightforward integration for most use cases.
#### 2. Enrich credentials via data functions & webhooks (after offer, before signing)
You can mark specific credential attributes as "to be filled later" and let the Issuer call [data functions](https://docs.walt.id/community-stack/issuer/credential-issuance/data-functions.md) before signing – including webhooks into your systems. Returned values are mapped into the credential right before issuance.
**This means:**
- Fetch dynamic or time-sensitive data at the last possible moment (e.g. timestamps, status flags, IDs, expiration dates).
- Call external APIs via webhooks for custom logic (e.g. fraud checks, entitlement checks, policy decisions) and use the result directly in the credential.
- Combine static attributes (provided up front) with dynamic attributes (via data functions) in a single issuance flow.
#### 3. Fetch identity data via authorization servers (OID4VCI authorization code flow)
When using the OID4VCI authorization code flow, the user is required to authenticate with an external Authorization Server / Identity Provider (IdP) before claiming a credential. After successful authentication, the Issuer API can use the resulting tokens to fetch user claims and map them into credential attributes.
**This means:**
- Use your existing identity infrastructure (e.g. Keycloak, Azure AD, custom OpenID Connect (OIDC) IdP) as the trusted identity source.
- Ensure that attributes are tied to a fresh, interactive authentication event (step-up, MFA, etc.).
- Align with established IAM policies (scopes, consents, attribute release policies) while issuing verifiable credentials.
**Info:**
**Coming Soon:** Full OID4VCI authorization code flow support with external IdP integration for dynamic claims fetching is currently in development for the Community Stack and will be available soon.
## FAQs
- **Can I mix different data sources in a single credential issuance flow?** — Yes. A single issuance can combine attributes that come from your backend, attributes returned via webhooks/data functions, and (coming soon) attributes obtained from an Authorization Server after user authentication.
- **Do I need an Identity Provider to use Data & Identity Sourcing?** — No. You can issue credentials entirely based on data from your backend or via webhooks. Authorization code flow with IdP integration for dynamic claims fetching is coming soon to the Community Stack.
- **Can I keep sensitive attributes out of the credential while still using them in decisions?** — Yes. You can use data functions and webhooks to call external systems that perform risk or eligibility checks. Only the decision (e.g. "eligible = true") has to be written into the credential; sensitive raw data can remain in your own systems.
- **Can I start simple and evolve my data sourcing strategy later?** — Yes. Many deployments start with a simple backend-only model and gradually introduce data functions and webhooks as requirements become more sophisticated – without changing the underlying credential formats or wallet integration.
## Get Started
- **[Issue a credential using pre-defined data](https://docs.walt.id/community-stack/issuer/credential-issuance/sd-jwt-vc-oid4vc.md)** – Step-by-step guide showing how to pass complete subject attributes from your backend when creating a credential offer (approach 1).
- **[Use data functions to enrich credentials dynamically](https://docs.walt.id/community-stack/issuer/credential-issuance/data-functions.md)** – Learn how to mark attributes for late binding and use webhooks or built-in functions to fetch data right before signing (approach 2).
--------------------------------------------------------------------------------
PAGE: Credential Branding
URL: https://docs.walt.id/community-stack/issuer/capabilities/credential-branding
RAW: https://docs.walt.id/community-stack/issuer/capabilities/credential-branding.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Credential branding
The Community Stack Issuer API lets you control how credentials appear to wallets and users when issued—setting claim labels, issuer logos, descriptions, and background/text colors (and localized variants) so wallets can render a consistent, branded credential. Use issuer-level metadata to keep visuals consistent across a credential type, or embed display data per credential instance when you need finer-grained differentiation (e.g., ticket tiers or program variants).
## What's included
- **Issuer metadata for each credential type**
Define colors, logo, description, and display fields once per credential type (e.g. digital diploma) so wallets can render predictable visuals without hard-coding styling.
- **Per-credential display overrides**
Embed display attributes in a specific credential instance when variants of the same type need distinct branding (e.g., VIP vs. standard ticket).
- **User-friendly display fields**
Provide human-friendly labels, value hints, and ordering for claims so wallets show meaningful attribute names instead of raw keys.
- **Localization for labels**
Supply localized display metadata so wallets can show claim labels and descriptions in the user's language.
## Include display metadata at issuance (data function)
You can pull configured display metadata into an issuance request using the `display` data function in the mapping object of the issuance request. This lets you embed visuals per credential instance without re-specifying them in the payload. Learn more in the [data functions documentation](https://docs.walt.id/community-stack/issuer/credential-issuance/data-functions.md).
Note: Embedding display metadata at issuance via the `display` data function is optional, as the wallet could also fetch the display information from the issuer metadata directly. It really depends on the implementation and the wallet you are using.
## FAQs
- **Will other wallets show branding reliably?** — Issuer metadata follows standardized display conventions, so it's the most interoperable way to share visuals (including localization). Per-credential embedded display data is not yet standardized; the walt.id wallet supports both approaches, but third-party wallets may only honor issuer metadata and ignore in-credential display info until they add support.
- **What is a credential type?** — A concrete subtype within a credential standard. For example, under [**W3C Verifiable Credential (W3C VC)**](https://docs.walt.id/concepts/digital-credentials/verifiable-credentials-w3c.md) you might have a digital diploma or digital ID; under [**ISO Mobile Driving License (mDL)**](https://docs.walt.id/concepts/digital-credentials/mdoc-mdl-iso.md) you might have an mDL or a photo ID; under [**Selective Disclosure JWT (SD-JWT VC)**](https://docs.walt.id/concepts/digital-credentials/sd-jwt-vc.md) you might have an identity credential. Each type can carry its own display metadata so wallets know how to present it.
## Get Started
- **[Configure credential display metadata](https://docs.walt.id/community-stack/issuer/configurations/config-files/credential-issuer-metadata.md)** – Define colors, logos, descriptions, and display fields for each credential type in the issuer metadata configuration file.
- **[Use display data functions](https://docs.walt.id/community-stack/issuer/credential-issuance/data-functions.md)** – Learn how to embed display metadata dynamically during credential issuance.
--------------------------------------------------------------------------------
PAGE: Credential Lifecycle
URL: https://docs.walt.id/community-stack/issuer/capabilities/credential-lifecycle
RAW: https://docs.walt.id/community-stack/issuer/capabilities/credential-lifecycle.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Credential lifecycle
The Community Stack Issuer API enables you to issue credentials that support lifecycle management by embedding status references and validity timestamps. You can issue credentials that reference status lists for revocation or suspension (using standards like TokenStatusList, Bitstring Status List v1.0), and include validity windows via validFrom and validUntil fields. The actual hosting and management of status credentials is handled by your own infrastructure.
## What's included
- **Embed status references in credentials**
The Community Stack can sign and issue credentials that include status field references pointing to external status lists. Supports standard formats (Bitstring Status List v1.0, TokenStatusList, StatusList2021) for tracking revocation, suspension, or custom states across credential types ([**W3C VC**](https://docs.walt.id/concepts/digital-credentials/verifiable-credentials-w3c.md), [**SD-JWT VC**](https://docs.walt.id/concepts/digital-credentials/sd-jwt-vc.md), [**ISO mdoc**](https://docs.walt.id/concepts/digital-credentials/mdoc-mdl-iso.md)). You provide the status list URL and bit position during issuance.
- **Set validity timestamps during issuance**
Configure validFrom and validUntil per credential—statically or via data functions—to control when a credential becomes valid and when it expires. These timestamps are embedded directly in the issued credential, requiring no external status infrastructure.
**Info:**
**Important:** The Community Stack can issue credentials with status information, but does not provide built-in hosting, management, or automatic updates of status credentials. You are responsible for hosting the status credential (status list) on your own infrastructure, updating it when credentials need to be revoked or suspended, re-signing after updates, and publishing the updated version so verifiers can access it. This differs from the Enterprise Stack, which includes a dedicated Credential Status Service that automatically handles all of these operations.
## Status Management in the Community Stack
When you issue a credential with a status field in the Community Stack:
1. **During issuance**: You specify the status credential URL and the bit position in the status list where this credential's status is tracked.
2. **After issuance**: You must implement your own system to:
- Host and serve the status credential (status list) at the URL you referenced
- Update the status list when credentials need to be revoked or suspended
- Re-sign the updated status list
- Publish the new version so verifiers can fetch it
The Issuer API handles signing the credential with the status reference, but lifecycle management is your responsibility.
## FAQs
- **Do verifiers need online connectivity?** — They typically fetch or cache the referenced status list. Offline verification works as long as the verifier has a fresh copy of the list; otherwise, it should treat status as unknown until it can refresh.
- **Can I use the Community Stack for production credential revocation?** — Yes, but you need to build the status credential hosting and management infrastructure yourself. For production deployments that need turnkey status management, consider the Enterprise Stack's Credential Status Service.
- **What status list formats are supported?** — The Community Stack can issue credentials that reference any of the major status list standards: Bitstring Status List v1.0, TokenStatusList, StatusList2021, and RevocationList2020.
## Get Started
- **[Use data functions for validity dates](https://docs.walt.id/community-stack/issuer/credential-issuance/data-functions.md)** – Learn how to set validFrom and validUntil timestamps dynamically.
--------------------------------------------------------------------------------
PAGE: Credential Templates, Types, and Schemas
URL: https://docs.walt.id/community-stack/issuer/capabilities/credential-templates-types-schemas
RAW: https://docs.walt.id/community-stack/issuer/capabilities/credential-templates-types-schemas.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Credential Templates, Types, and Schemas
Credential Templates, Types, and Schemas explains how the Community Stack models what kind of credential you issue (type) and in which format—while letting you define the “template” logic via your own own system.
## What's included
The Community Stack supports:
- Multiple credential **formats** (e.g. [**W3C VC**](https://docs.walt.id/concepts/digital-credentials/verifiable-credentials-w3c.md), [**SD-JWT VC**](https://docs.walt.id/concepts/digital-credentials/sd-jwt-vc.md), [**ISO mdoc/mDL**](https://docs.walt.id/concepts/digital-credentials/mdoc-mdl-iso.md)).
- Arbitrary credential **types** per format (e.g. DigitalDiploma, BankID, identity_credential, PhotoID).
- Flexible **data structures** controlled by you (or by external schemas/standards, depending on the format).
- Alignment with **type metadata** concepts (e.g. SD-JWT VC vct Type Metadata) for future schema enforcement and display definitions.
#### 1. Credential Formats
A **credential format** defines how the credential is structured, serialized, and signed (for example, which fields are required and which cryptographic primitives are used).
Examples include:
- W3C Verifiable Credential (VC)
- SD-JWT VC (IETF)
- mdoc / mDL (ISO 18013-5)
When you issue a credential via the Community Stack API, you choose one of these formats so the issuer knows how to construct and sign the resulting credential.
#### 2. Credential Types
A **credential type** answers: "What is this credential?" – for example:
- DigitalDiploma, EmployeeID, BankID, eID for W3C VC.
- identity_credential, age_over_18, residency_credential for SD-JWT VC (IETF).
- org.iso.18013.5.1.mDL or PhotoID for mdoc/mDL-based credentials.
In the Community Stack, you:
- Define **credential types** in the [issuer metadata configuration](https://docs.walt.id/community-stack/issuer/configurations/config-files/credential-issuer-metadata.md) together with:
- The **format** (W3C VC, SD-JWT VC, mdoc, …).
- Optional type-level **metadata** (e.g. branding).
- Reference these types in issuance requests.
Once a type is configured, any issuance flow can issue credentials "of that type" in the chosen format. The issuer does not hard-code which attributes that type must contain – unless the underlying format or external standards require it.
#### 3. Data Structures & Schemas
Where traditional "template engines" define schema inside the issuer product, the Community Stack assumes:
- Schemas are defined and managed in your own systems, not hard-coded into the issuer.
- The issuer will sign whatever payload you provide, as long as:
- It is valid for the selected format, and
- It respects any external schema/standard you use.
In detail:
**W3C VC**
- You can define your own schemas (for example using JSON Schema) for the claims you want to include.
- You design the structure needed for a given type (e.g. BankID: iban, bankName, holderName, riskLevel, …).
- The Community Stack checks that the credential is a valid W3C VC in JWT/SD-JWT form; it does not impose extra "template rules" on top.
**SD-JWT VC (IETF)**
- You decide which claims exist and which can be selectively disclosed.
- The SD-JWT VC spec introduces:
- `vct` (Verifiable Credential Type) – an identifier for the credential type.
- Type Metadata – which describes which claims MUST / MAY appear and optional display properties.
- This gives you a standards-based way to express "templates" for SD-JWT VCs that wallets and verifiers can understand.
- In the Community Stack, you can freely define and use your own `vct` values. If you rely on externally hosted Type Metadata documents, your own systems are responsible for ensuring that issuance payloads conform to those definitions.
**ISO mdoc**
- The mdoc/mDL format comes with prescribed data elements and namespaces (e.g. org.iso.18013.5.1.mDL).
- Some attributes are mandatory (e.g. family name, birth date); others are optional.
- The issuer enforces the format-level rules, but does not add an additional, proprietary template layer.
At the end, **you are not limited in what attributes you put into a credential**, as long as they comply with the rules of the chosen format and any external schema or Type Metadata you've decided to follow.
## "Templates" Without a Templating Engine
So how do you get template-like behavior without a "template" feature in the Community Stack?
You combine three layers:
1. **Configuration level (in the Community Stack)**
- Define credential types + formats in your [issuer metadata configuration](https://docs.walt.id/community-stack/issuer/configurations/config-files/credential-issuer-metadata.md).
- Optionally configure display metadata for labels and branding.
- Reference these types in your issuance requests.
2. **Application level (in your backend)**
- Maintain your own schemas or data models for each type.
- Validate data structure and assemble the final credential payload before calling the Issuer API.
- Reuse structures and mappings across your applications (effectively your "templates").
3. **Standards level**
- For **SD-JWT VC**, use `vct` and Type Metadata to describe schemas and display rules.
- For **mdoc / mDL**, rely on ISO-defined docTypes and data element definitions.
- For **W3C VC**, use JSON schemas or other schema languages your stack already uses.
## FAQs
- **Do you support credential templates?**
We don't ship a built-in "template engine" where schemas are hard-coded into the issuer. Instead, we give you a flexible model of formats + types + schemas. You model "templates" in your own systems (or via open standards like SD-JWT VC Type Metadata), and the Community Stack issues credentials based on those definitions.
- **What's the difference between a credential format and a credential type?**
A format defines how the credential is encoded and signed (e.g. W3C VC in JWT/SD-JWT form, SD-JWT VC, mdoc). A type defines what the credential represents (e.g. DigitalDiploma, BankID, identity_credential). You usually define type/format pairs in your issuer metadata.
- **Can I restrict which attributes are allowed for a given credential type?**
Yes – but this restriction lives in your schemas and rules, not in a rigid built-in template. You can:
- Validate against your own schema before calling the Issuer API.
- Use SD-JWT VC Type Metadata (vct) to describe required/optional claims and display rules.
- **Do you limit which claims I can put into a credential?**
No. As long as the credential is valid for the chosen format (and any external schema/standard you use), you are free to include the claims that make sense for your use case. For mdoc/mDL, you must respect the mandatory/optional attributes defined in the ISO standards.
## Get Started
- **[Configure issuer metadata](https://docs.walt.id/community-stack/issuer/configurations/config-files/credential-issuer-metadata.md)** – Learn how to define credential types and formats in the issuer metadata configuration file.
- **[Issue your first credential](https://docs.walt.id/community-stack/issuer/credential-issuance/sd-jwt-vc-oid4vc.md)** – Step-by-step guide showing how to issue credentials using your configured types.
--------------------------------------------------------------------------------
PAGE: Credential Delivery
URL: https://docs.walt.id/community-stack/issuer/capabilities/credential-delivery
RAW: https://docs.walt.id/community-stack/issuer/capabilities/credential-delivery.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Credential Delivery
Credential Delivery describes how a credential gets from the Community Stack Issuer API into the holder's wallet. It focuses on the protocol flows used to issue credentials (based on [OpenID for Verifiable Credential Issuance, OID4VCI](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md)) and the device journeys (same-device vs cross-device) used to deliver credential offers.
The Community Stack does not constrain how you integrate issuance into your applications. It exposes simple API endpoints that return an OID4VCI standard compliant credential offer URL. You decide whether that URL becomes a QR code, a deep link behind a button, or both.
**Note:**
"Which OID4VCI flow do we use, and how does the credential offer actually reach the user's wallet – on the same device or across devices?"
## What's included
The Community Stack supports different **issuance flows** and **device journeys** for delivering credentials. These two dimensions are independent and can be combined freely.
**Issuance flows (how users prove they're eligible and how the wallet gets permission to fetch the credential):**
- Pre-Authorized Code Flow (with and without PIN)
- Authorization Code Flow (coming soon)
**Device journeys (how the credential offer reaches the user's wallet – on the same device or across devices):**
- Same-device credential offers (link / button)
- Cross-device credential offers (QR code scanned from another device)
#### 1. Pre-Authorized Code Issuance Flow (with and without PIN)
In the **pre-authorized code issuance flow**, the issuer has already verified the user's eligibility (via your backend, existing session, or business logic). The credential offer is generated for a specific user and includes a pre-authorized code that grants the wallet immediate permission to fetch the credential – no interactive authentication required by the receiver of the credential.
The Community Stack supports both variants defined by OID4VCI:
- **Without PIN** – the wallet uses the pre-authorized code directly to get permission and request the credential.
- **With PIN** – the offer indicates that a user PIN (transaction code) is required. The PIN is delivered via a separate channel (e.g. email, SMS, in-person) and must be entered in the wallet when claiming the credential.
**This means:**
- Eligibility is determined upstream (e.g. back-office process, portal, CRM) before the offer is created.
- You can use existing secure channels (logged-in portals, emails, SMS, letters) to deliver offers and PINs.
- You can add a "second factor" for sensitive credentials (pre-authorized code + PIN).
- You avoid re-authenticating the user at issuance time when you already trust your upstream process.
#### 2. Authorization Code Issuance Flow
In the **authorization code issuance flow**, the credential offer can be shared broadly (e.g. any user visiting a page), but the user must authenticate with an external Authorization Server / Identity Provider (IdP) before they can receive the credential.
**This means:**
- The offer can be long-lived and reused by any eligible user.
- Users prove they're eligible by signing in at your IdP (e.g. Keycloak, Azure AD, custom OpenID Connect (OIDC) IdP) during the flow.
- After successful authentication, the wallet exchanges the authorization code for a token and then uses that permission to request the credential from the Issuer API.
- You can combine this with [Data & Identity Sourcing](https://docs.walt.id/community-stack/issuer/capabilities/data-sourcing.md) to fetch user claims from the Authorization Server and inject them into the credential at issuance time.
**Info:**
**Coming Soon:** Full authorization code flow support with external IdP integration is currently in development for the Community Stack and will be available soon.
### 3. Device Journeys: Same-device & Cross-device Offers
Regardless of which **issuance flow** you use (pre-authorized code or authorization code), the **device journey** defines how the user encounters and accepts the credential offer:
#### Cross-device flow
- The credential offer URL is encoded as a **QR code** on one device (e.g. desktop web application, kiosk).
- The user scans the QR code with their mobile wallet and continues the flow on their phone.
- This is ideal when the user interacts with an issuer on a desktop/laptop, but the wallet lives on a smartphone.
#### Same-device flow
- The credential offer URL is embedded in a link or button on the same device that runs the wallet.
- The user taps the link (e.g. in an email or web page on their phone), which opens the wallet and displays the credential offer.
- This is ideal when everything happens on a single device (e.g. mobile-first experiences).
**With the Community Stack:**
- The Issuer API returns a standards-compliant OID4VCI **credential offer URL**.
- You are free to:
- Turn that URL into a QR code (cross-device).
- Use it directly as a deep link (same-device).
- Offer both options in parallel.
- The choice of same-device vs cross-device is **independent** of the issuance flow (pre-authorized code or authorization code, with or without PIN).
## FAQs
- **Is a PIN required for the pre-authorized code issuance flow?** - No. The PIN is optional. In the Community Stack, you can configure pre-authorized offers with or without a PIN and decide when a second factor (transaction code) is appropriate for your use case.
- **What's the difference between same-device and cross-device flows?** - In a same-device journey, the user taps a link on the same device that holds the wallet (e.g. from an email on their phone). In a cross-device journey, the user interacts with the issuer on one device (e.g. desktop) and scans a QR code with a wallet on another device (e.g. phone). Both can be used with any issuance flow and require no extra issuer configuration beyond generating the offer URL.
- **Does the Community Stack require a specific wallet?** - No. The issuer API uses standard OID4VCI endpoints and credential offer semantics. Any wallet that implements the relevant OID4VCI versions and issuance flows can claim credentials from the Community Stack Issuer API.
- **How do QR codes relate to OID4VCI issuance flows?**
In OpenID for Verifiable Credential Issuance (OID4VCI), a QR code is simply a user-friendly way to deliver an issuance URL to a wallet app – typically a **credential offer URL** (and, depending on the flow, parameters such as a pre-authorized code). The QR code itself usually just encodes this URL. When the user scans it with their wallet, the wallet opens the URL and continues the OID4VCI issuance flow (pre-authorized code or authorization code, with or without PIN). In same-device journeys, the same URL is usually opened via a link or button instead of a QR code. So QR codes are not a separate OID4VCI feature, but a UX pattern for starting issuance flows, especially in cross-device scenarios.
## Get Started
- **[Issue your first credential](https://docs.walt.id/community-stack/issuer/credential-issuance/sd-jwt-vc-oid4vc.md)** – Step-by-step guide showing how to generate credential offers using the pre-authorized code flow.
- **[Configure credential data sourcing](https://docs.walt.id/community-stack/issuer/capabilities/data-sourcing.md)** – Learn how to populate credential claims from your data sources.
--------------------------------------------------------------------------------
PAGE: Setup
URL: https://docs.walt.id/community-stack/issuer/setup
RAW: https://docs.walt.id/community-stack/issuer/setup.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Setup
## Installation & Running the Project
Make sure you have Docker installed on your machine.
### Option 1: Using Docker Pull
**1. Cloning The Identity-Repo**
First, clone the repository to your local machine:
```bash
git clone https://github.com/walt-id/waltid-identity.git && cd waltid-identity
```
**2. Pulling the Docker Image**
To quickly get started, you can pull the pre-built Docker image from Docker Hub:
```bash
docker pull waltid/issuer-api
```
**2. Running the Docker Image**
Once the image is pulled, you can run it using the following command:
```bash
docker run -p 7002:7002 \
-itv $(pwd)/waltid-services/waltid-issuer-api/config:/waltid-issuer-api/config \
-t waltid/issuer-api
```
### Option 2: Building the Image Yourself
**1. Cloning The Identity-Repo**
First, clone the repository to your local machine:
```bash
git clone https://github.com/walt-id/waltid-identity.git && cd waltid-identity
```
**2. Building the Docker Image**
If you prefer to build the Docker image yourself, use the Gradle Ktor/Jib tasks:
```bash
# Development (local Docker daemon, single-arch)
./gradlew :waltid-services:waltid-issuer-api:publishImageToLocalRegistry
# image: waltid/issuer-api:
```
```bash
# Production (multi-arch push to your registry)
export DOCKER_USERNAME=
export DOCKER_PASSWORD=
./gradlew :waltid-services:waltid-issuer-api:publishImage
# image: docker.io//issuer-api:
```
Note: multi-arch images require a registry push. Local tar output is single-arch only.
**3. Running the Docker Image**
After building the image, run it using (adjust the image name if you pushed to your registry):
```bash
docker run -p 7002:7002 \
-itv $(pwd)/waltid-services/waltid-issuer-api/config:/waltid-issuer-api/config \
-t waltid/issuer-api
```
## Start Issuing Credentials
Now that the issuer api is running, you can start issuing credentials.
- [How to Issue W3C Verifiable Credentials (JWT / SD-JWT) via OID4VCI](https://docs.walt.id/community-stack/issuer/credential-issuance/vc-oid4vc.md)
- [How to Issue SD-JWT-based Verifiable Credentials (IETF | SD-JWT VC) via OID4VCI](https://docs.walt.id/community-stack/issuer/credential-issuance/sd-jwt-vc-oid4vc.md)
- [How to Issue Mobile Driver’s Licenses (ISO/IEC 18013-5 MDLs) via OID4VCI](https://docs.walt.id/community-stack/issuer/credential-issuance/mdl-oid4vc.md)
- [Full API Reference](https://issuer.demo.walt.id/swagger/index.html)
--------------------------------------------------------------------------------
PAGE: Overview
URL: https://docs.walt.id/community-stack/issuer/key-management/overview
RAW: https://docs.walt.id/community-stack/issuer/key-management/overview.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Overview
The issuer API is designed to be stateless and does not store cryptographic key material. Therefore, every API call
requires the cryptographic key to be provided. When signing and issuing a credential e.g. via [**OID4VCI**](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md), either the complete
key in JWK format or a reference to a key stored in an external KMS solution is
needed. In production systems, the recommended approach is to use an external KMS solution, so that the private key used
to sign a credential never leaves a secure environment.
**Supported KMS Solutions**
- [HashiCorp Vault](https://developer.hashicorp.com/vault)
- [AWS KMS](https://aws.amazon.com/kms/)
- [Azure Key Vault](https://azure.microsoft.com/en-us/products/key-vault/)
## Get Started
- [HashiCorp Vault](https://docs.walt.id/community-stack/issuer/key-management/hashicorp-vault.md) - Use keys from HashiCorp Vault to sign credentials
- [AWS KMS](https://docs.walt.id/community-stack/issuer/key-management/aws-kms/overview.md) - Use keys from AWS KMS to sign credentials
- [Azure Key Vault](https://docs.walt.id/community-stack/issuer/key-management/azure-kms/overview.md) - Use keys from Azure Key Vault to sign credentials
--------------------------------------------------------------------------------
PAGE: Local
URL: https://docs.walt.id/community-stack/issuer/key-management/local
RAW: https://docs.walt.id/community-stack/issuer/key-management/local.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Local
**You can create a private and public key using the issuer API** using one of the following algorithms: ed25519,
secp256k1, secp256r1, or RSA. However, **please note that the issuer API does not automatically store any cryptographic
key material.** It is your responsibility to store the key that is returned and provide it in JWK format for future
signing and issuance operations.
**If you are working in a production setup, it is highly recommended to use KMS providers like Hashicorp Vault to secure
your key material**. This will ensure that your secrets are never exposed outside of a secure environment, reducing the
risk of key compromises. For a list of KMS integrations that are compatible with walt.id, please visit
the [overview page](https://docs.walt.id/community-stack/issuer/key-management/overview.md).
## Key Creation
To create a key and get an associated [**Decentralized Identifier (DID)**](https://docs.walt.id/concepts/decentralised-identifiers.md) at the same time, we will be utilizing the `/onboard/issuer` endpoint.
**Option: CURL**
**Endpoint:** `/onboard/issuer` | [API Reference](https://issuer.demo.walt.id/swagger/index.html#/Issuer%20onboarding/post_onboard_issuer)
**Example Request**
```bash
curl -X 'POST' \
'http://0.0.0.0:7002/onboard/issuer' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"key": {
"backend": "jwk",
"keyType": "secp256r1"
},
"did": {
"method": "jwk"
}
}'
```
**Body**
```json
{
"key": {
"backend": "jwk",
"keyType": "secp256r1"
},
"did": {
"method": "jwk"
}
}
```
**Body Parameters**
- `key`
- `backend`: _String_ - Specifies the storage type of key. It can be `jwk` (manged by you), `TSE` (managed by
Hashicorp
Vault) and others. Learn more about different types [here](https://docs.walt.id/community-stack/issuer/key-management/overview.md).
- `keyType`: _String_ - the algorithm used to generate the key. For local, it can
be ed25519, secp256k1, secp256r1, or RSA. For the other types and the supported algorithms, please
go [here](https://docs.walt.id/community-stack/issuer/key-management/overview.md).
- `did`:
- `method`: _String_ - Specifies the DID method. It can be key, jwk, web, cheqd.
---
**Example Response**
The onboard/issuer endpoint will return an object containing both the generated key in JWK format and the related DID.
```json
{
"issuerKey": {
"type": "jwk",
"jwk": "{\"kty\":\"EC\",\"d\":\"nnITt7w11Gehk_oA9bxOQrz1GGzMECiBNoVejKOn2CA\",\"crv\":\"P-256\",\"kid\":\"fQod7zGbGsO14yVnNT-cslwFyKgEPjKsYHQIfGqQMFI\",\"x\":\"EJHmd2wZpmLCnmN49bQeOSn6NDb8kfeXrfpr1K1U8FY\",\"y\":\"Wxzi0fayJU80JJZOl1-XUrZzEU5AcAQxUdO69-gmBmc\"}"
},
"issuerDid": "did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2Iiwia2lkIjoiZlFvZDd6R2JHc08xNHlWbk5ULWNzbHdGeUtnRVBqS3NZSFFJZkdxUU1GSSIsIngiOiJFSkhtZDJ3WnBtTENubU40OWJRZU9TbjZORGI4a2ZlWHJmcHIxSzFVOEZZIiwieSI6Ild4emkwZmF5SlU4MEpKWk9sMS1YVXJaekVVNUFjQVF4VWRPNjktZ21CbWMifQ"
}
```
--------------------------------------------------------------------------------
PAGE: HashiCorp Vault Transit Secrets Engine
URL: https://docs.walt.id/community-stack/issuer/key-management/hashicorp-vault
RAW: https://docs.walt.id/community-stack/issuer/key-management/hashicorp-vault.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# HashiCorp Vault Transit Secrets Engine
Video: https://youtu.be/htSvJ-rIHUk
The issuer API can leverage HashiCorp
Vault's [Transit Secrets Engine](https://developer.hashicorp.com/vault/docs/secrets/transit)
to hook into the "cryptography as a service" or "encryption as a service" features provided
by [HashiCorp Vault](https://developer.hashicorp.com/vault). With
that, the issuer API can sign and verify credentials whilst keeping the secrets (signing keys) in an external
secure environment.
If you are new to HashiCorp Vault, make sure to check out the
guides [here](https://developer.hashicorp.com/vault/tutorials/getting-started/getting-started-intro).
The rest of this document assumes you already know how Vault works and have a transit secrets engine setup and
running.
## Setting Up Authentication Method
Once you have the HashiCorp Server up and running and enabled the transit secrets engine mentioned above, we need to
either enable AppRole Authentication or User/Pass Authentication to use HashiCorp Vault with the walt.id issuer API.
### Setup Policy
Before enabling and configuring authentication methods, a policy must be created to define the permissions and actions
that authenticated users or roles are allowed to perform.
**Create The Policy File**
```bash
echo 'path "transit/*" {
capabilities = ["create", "update", "read", "delete", "list"]
}' > transit-policy.hcl
```
**Check the Client Configuration**
If you are configuring vault locally make sure that the client is configured to
communicate via HTTP. This can be achieved by setting the `VAULT_ADDR` environment
variable.
```bash
export VAULT_ADDR=https://127.0.0.1:8200
```
**Upload the Policy**
```bash
vault policy write transit-policy transit-policy.hcl
# Expected result:
# Success! Uploaded policy: transit-policy
```
### Setup Auth Method
**Option: AppRole Auth**
To enable AppRole auth you have two options, the Web UI of HashiCorp Vault or the CLI:
**Option: Web UI**
1. Navigate to `Access`.
2. Choose `Authentication Methods`.
3. Click on `Enable new method`.
4. Select `Generic / AppRole`.
5. Click `Enable method`.
_Please note: You might want to disable lockout: select the created approle method,
press "Configure", scroll to "User lockout configuration" and select "Disable lockout for this mount"._
**Option: CLI**
In the CLI enter:
```bash
vault auth enable approle
```
_Please note: You might want to disable lockout: Use the `disable_lockout` parameter._
### Configure AppRole
**Add a Role**
```bash
vault write auth/approle/role/my-role token_type=batch
# Additional parameters can include:
# secret_id_ttl, token_num_uses, token_ttl, token_max_ttl, secret_id_num_uses
```
**Show Role ID**
```bash
vault read auth/approle/role/my-role/role-id
# Save the value of role_id for later use.
```
**Issue a Secret ID**
```bash
vault write -f auth/approle/role/my-role/secret-id
# Save the value of secret_id for later use.
```
**Associate Policy with AppRole**
```bash
vault write auth/approle/role/my-role token_policies="transit-policy"
# Expected result:
# Success! Data written to: auth/approle/role/my-role
```
If you already have a token policy with the AppRole:
```bash
vault write auth/approle/role/my-role token_policies="existing-policy,transit-policy"
```
**Option: User/Pass Auth**
**Option: CLI**
**Enable Userpass**
```bash
vault auth enable userpass
# Alternatively with a custom path
vault auth enable -path= userpass
# Expected result:
# Success! Enabled userpass auth method at: userpass/
```
**Add User**
```bash
vault write auth//users/myuser password=mypassword policies=mypolicy
# Creates user "myuser", password "mypassword", associated with "mypolicy".
# Example policy: transit-policy (see above).
```
## Key Creation
To create the key you can use the onboard endpoint provided by the issuer API or the CLI, UI or the API from
Vault's [Transit Secrets Engine](https://developer.hashicorp.com/vault/docs/secrets/transit).
The issuer API accepts walt.id `keyType` values and maps them to the matching HashiCorp Vault Transit key type before creating or resolving the key.
| walt.id `keyType` | Vault Transit key type |
|---|---|
| `Ed25519` | `ed25519` |
| `secp256r1` | `ecdsa-p256` |
| `secp384r1` | `ecdsa-p384` |
| `secp521r1` | `ecdsa-p521` |
| `RSA` | `rsa-2048` |
| `RSA3072` | `rsa-3072` |
| `RSA4096` | `rsa-4096` |
`secp256k1` is not supported by the TSE integration. See the HashiCorp Vault Transit Secrets Engine key types [full list](https://developer.hashicorp.com/vault/docs/secrets/transit#key-types) for the provider-side names.
### Creation via Issuer API
**Option: CURL**
**Endpoint:** `/onboard/issuer` | [API Reference](https://issuer.demo.walt.id/swagger/index.html#/Issuer%20onboarding/post_onboard_issuer)
**Example Request**
**Option: AppRole Auth**
```bash
curl -X 'POST' \
'https://issuer.demo.walt.id/onboard/issuer' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"key": {
"backend": "tse",
"keyType": "Ed25519",
"config": {
"server": "http://127.0.0.1:8200/v1/transit",
"auth": {
"roleId": "my_role",
"secretId": "my_secret"
},
"namespace": "optional"
}
},
"did": {
"method": "key"
}
}'
```
**Body**
```json
{
"key": {
"backend": "tse",
"keyType": "Ed25519",
"config": {
"server": "http://127.0.0.1:8200/v1/transit",
"auth": {
"roleId": "my_role",
"secretId": "my_secret"
},
"namespace": "optional"
}
},
"did": {
"method": "key"
}
}
```
**Body Parameters**
- `key`
- `backend`: _String_ - Specifies the storage type of key. It can be `jwk` (managed by you), `TSE` (managed by
HashiCorp Vault) and others. Learn more about different types [here](https://docs.walt.id/community-stack/issuer/key-management/overview.md).
- `keyType`: _String_ - the algorithm used to generate the key. For Vault TSE, use one of `Ed25519`, `secp256r1`, `secp384r1`, `secp521r1`, `RSA`, `RSA3072`, or `RSA4096`.
- `config`
- `server`: _URL_ - The endpoint of your Vault instance. Following the structure of
`https:///v1/`, e.g. "https://vault.walt.id/v1/transit". By default the transit
engine will live at /transit.
- `auth`:
- **AppRole Auth:**
- `roleId`: _String_ - The Role ID for AppRole authentication.
- `secretId`: _String_ - The Secret ID for AppRole authentication.
- **User/Pass Auth:**
- `username`: _String_ - The username for user/pass authentication.
- `password`: _String_ - The password for user/pass authentication.
- **AccessToken Auth (Deprecated):**
- `accessKey`: _String_ - The access token for token-based authentication.
- `namespace`: _String_ (optional) - The namespace within the Vault instance.
- `did`:
- `method`: _String_ - Specifies the DID method. It can be key, jwk, web, cheqd.
**Option: User/Pass Auth**
```bash
curl -X 'POST' \
'https://issuer.demo.walt.id/onboard/issuer' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"key": {
"backend": "tse",
"keyType": "Ed25519",
"config": {
"server": "http://127.0.0.1:8200/v1/transit",
"auth": {
"username": "foo",
"password": "bar"
},
"namespace": "optional"
}
},
"did": {
"method": "key"
}
}'
```
**Body**
```json
{
"key": {
"backend": "tse",
"keyType": "Ed25519",
"config": {
"server": "http://127.0.0.1:8200/v1/transit",
"auth": {
"username": "foo",
"password": "bar"
},
"namespace": "optional"
}
},
"did": {
"method": "key"
}
}
```
**Body Parameters**
- `key`
- `backend`: _String_ - Specifies the storage type of key. It can be `jwk` (managed by you), `TSE` (managed by
HashiCorp Vault) and others. Learn more about different types [here](https://docs.walt.id/community-stack/issuer/key-management/overview.md).
- `keyType`: _String_ - the algorithm used to generate the key. For Vault TSE, use one of `Ed25519`, `secp256r1`, `secp384r1`, `secp521r1`, `RSA`, `RSA3072`, or `RSA4096`.
- `config`
- `server`: _URL_ - The endpoint of your Vault instance. Following the structure of
`https:///v1/`, e.g. "https://vault.walt.id/v1/transit". By default the transit
engine will live at /transit.
- `auth`:
- **AppRole Auth:**
- `roleId`: _String_ - The Role ID for AppRole authentication.
- `secretId`: _String_ - The Secret ID for AppRole authentication.
- **User/Pass Auth:**
- `username`: _String_ - The username for user/pass authentication.
- `password`: _String_ - The password for user/pass authentication.
- **AccessToken Auth (Deprecated):**
- `accessKey`: _String_ - The access token for token-based authentication.
- `namespace`: _String_ (optional) - The namespace within the Vault instance.
- `did`:
- `method`: _String_ - Specifies the DID method. It can be key, jwk, web, cheqd.
**Option: AccessToken Auth (Deprecated)**
```bash
curl -X 'POST' \
'https://issuer.demo.walt.id/onboard/issuer' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"key": {
"backend": "tse",
"keyType": "Ed25519",
"config": {
"server": "http://127.0.0.1:8200/v1/transit",
"auth": {
"accessKey": "123456"
},
"namespace": "optional"
}
},
"did": {
"method": "key"
}
}'
```
**Body**
```json
{
"key": {
"backend": "tse",
"keyType": "Ed25519",
"config": {
"server": "http://127.0.0.1:8200/v1/transit",
"auth": {
"accessKey": "123456"
},
"namespace": "optional"
}
},
"did": {
"method": "key"
}
}
```
**Body Parameters**
- `key`
- `backend`: _String_ - Specifies the storage type of key. It can be `jwk` (managed by you), `TSE` (managed by
HashiCorp Vault) and others. Learn more about different types [here](https://docs.walt.id/community-stack/issuer/key-management/overview.md).
- `keyType`: _String_ - the algorithm used to generate the key. For Vault TSE, use one of `Ed25519`, `secp256r1`, `secp384r1`, `secp521r1`, `RSA`, `RSA3072`, or `RSA4096`.
- `config`
- `server`: _URL_ - The endpoint of your Vault instance. Following the structure of
`https:///v1/`, e.g. "https://vault.walt.id/v1/transit". By default the transit
engine will live at /transit.
- `auth`:
- **AppRole Auth:**
- `roleId`: _String_ - The Role ID for AppRole authentication.
- `secretId`: _String_ - The Secret ID for AppRole authentication.
- **User/Pass Auth:**
- `username`: _String_ - The username for user/pass authentication.
- `password`: _String_ - The password for user/pass authentication.
- **AccessToken Auth (Deprecated):**
- `accessKey`: _String_ - The access token for token-based authentication.
- `namespace`: _String_ (optional) - The namespace within the Vault instance.
- `did`:
- `method`: _String_ - Specifies the DID method. It can be key, jwk, web, cheqd.
---
**Example Response**
The onboard/issuer endpoint will return an object containing both the generated key in JWK format and the related DID.
```json
{
"issuerKey": {
"type": "tse",
"server": "http://127.0.0.1:8200/v1/transit",
"auth": {
"roleId": "b50427bd-5070-0bec-3a00-dfe470ba349d",
"secretId": "22ffc0f4-c308-e6d6-f5a4-246cd89bc17c"
},
"id": "k732628061",
"_publicKey": [
86,
-55,
122,
-77,
109,
-60,
61,
-40,
-122,
-57,
-101,
-53,
69,
-125,
12,
92,
-9,
89,
61,
-78,
116,
-76,
111,
-58,
-63,
-122,
-26,
79,
-110,
-32,
-73,
-98
],
"_keyType": "Ed25519"
},
"issuerDid": "did:key:z6MkkJ3JAhCpSGe5QD9UQ2WGzna7kcswa9ahMC2o3Dd1KRXb"
}
```
**Properties**
- `type`: _String_ - the type of key can be either "tse" when using HashiCorp Vault or "jwk" when providing the key in
full as JWK.
- `server`: _URL_ - The endpoint of your Vault instance. Following the structure
of `https:///v1/`, e.g. "https://vault.walt.id/v1/transit". By default
the transit engine will live at `/transit`.
- `auth`:
- **AppRole Auth:**
- `roleId`: _String_ - The Role ID for AppRole authentication.
- `secretId`: _String_ - The Secret ID for AppRole authentication.
- **User/Pass Auth:**
- `username`: _String_ - The username for user/pass authentication.
- `password`: _String_ - The password for user/pass authentication.
- **AccessToken Auth (Deprecated):**
- `accessKey`: _String_ - The access token for token-based authentication.
- `id`: _String_ - the ID of the key in the Transit Engine.
- `_publicKey` (optional): _Array_ - The public key can be fetched by the issuer API or directly provided, saving
resources and reducing network requests.
- `_keyType` (optional): _String_ - The key type can be fetched by the issuer API or directly provided, saving resources
and reducing network requests.
## Key Usage
Once you have successfully created a key that is one of the supported types listed above, you can use it in
sign and issue operations offered by the issuer API.
If you've already had a look at
our `/sign`, `/issue`, `/batchIssue`
endpoints, you have seen that they all follow a similar request body structure, where the key that should be
used for signing credentials is provided via the `issuerKey` property. Now instead of providing the key as JWK, we
provide
a reference to a key stored in Vault with the required parameters and access credentials.
Below you can see an example of
`issuerKey` object referencing a key stored in Vault.
**Option: Example IssuerKey Object**
```json
{
"type": "tse",
"server": "https://vault.walt.id/v1/transit",
"auth": {
"roleId": "b50427bd-5070-0bec-3a00-dfe470ba349d",
"secretId": "22ffc0f4-c308-e6d6-f5a4-246cd89bc17c"
},
"id": "k1474327600",
"_publicKey": [
-17,
83,
1,
26,
9,
-104,
-64,
-50,
48,
92,
-112,
-76,
7,
90,
-40,
-100,
72,
106,
-51,
-58,
64,
-26,
73,
121,
26,
-118,
39,
-8,
-25,
-34,
82,
37
],
"_keyType": "Ed25519"
}
```
**Properties**
- `type`: _String_ - the type of key can be either "tse" when using HashiCorp Vault or "jwk" when providing the key in
full as JWK.
- `server`: _URL_ - The endpoint of your Vault instance. Following the structure
of `https:///v1/`, e.g. "https://vault.walt.id/v1/transit". By default
the transit engine will live at `/transit`.
- `auth`:
- **AppRole Auth:**
- `roleId`: _String_ - The Role ID for AppRole authentication.
- `secretId`: _String_ - The Secret ID for AppRole authentication.
- **User/Pass Auth:**
- `username`: _String_ - The username for user/pass authentication.
- `password`: _String_ - The password for user/pass authentication.
- **AccessToken Auth (Deprecated):**
- `accessKey`: _String_ - The access token for token-based authentication.
- `id`: _String_ - the ID of the key in the Transit Engine.
- `_publicKey` (optional): _Array_ - The public key can be fetched by the issuer API or directly provided, saving
resources and reducing network requests.
- `_keyType` (optional): _String_ - The key type can be fetched by the issuer API or directly provided, saving resources
and reducing network requests.
## Example Issuance Request
Below you can see example issuance request
to [`jwt/issue`](https://issuer.demo.walt.id/swagger/index.html#/Credential%20Issuance/post_openid4vc_jwt_issue) using
a key created in the Vault Transit Secrets Engine to
sign the credential.
**Option: CURL**
[Api Reference](https://issuer.demo.walt.id/swagger/index.html#/Credential%20Issuance/post_openid4vc_jwt_issue)
```bash
curl -X 'POST' \
'https://issuer.demo.walt.id/openid4vc/jwt/issue' \
-H 'accept: text/plain' \
-H 'statusCallbackUri: https://example.com/$id' \
-H 'Content-Type: application/json' \
-d '{
"issuerKey": {
"type": "tse",
"server": "https://vault.walt.id/v1/transit",
"auth": {
"roleId": "b50427bd-5070-0bec-3a00-dfe470ba349d",
"secretId": "22ffc0f4-c308-e6d6-f5a4-246cd89bc17c"
},
"id": "k-1481938705"
},
"issuerDid": "did:key:z6MkjoRhq1jSNJdLiruSXrFFxagqrztZaXHqHGUTKJbcNywp",
"credentialData": {
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://purl.imsglobal.org/spec/ob/v3p0/context.json"
],
"id": "urn:uuid:THIS WILL BE REPLACED WITH DYNAMIC DATA FUNCTION (see below)",
"type": [
"VerifiableCredential",
"OpenBadgeCredential"
],
"name": "JFF x vc-edu PlugFest 3 Interoperability",
...
},
"mapping": {
"id": "",
...
}
}'
```
**Header Parameters**
- `statusCallbackUri`: _URL_ - Receive updates on the created issuance process, e.g. when a credential was successfully
claimed. The parameter expects a URL which can accept a JSON POST request. The URL can also hold a `$id`, which will
be
replaced by the issuance session id. For example: `https://myurl.com/$id`, `https://myurl.com`
or `https://myurl.com/test/$id`
Expand To Learn More
**Body**
The data send to the provided URL will contain a JSON body:
- `id` : _String_ - the issuance session id
- `type`: _String_ - the event type
- `data`: _JsonObject_ - the data for the event
**Event Types**
Possible events (event types) and their data are:
- `resolved_credential_offer` with the credential offer as JSON (in our Web Wallet: called when the issuance offer is
entered into the wallet, but not processing / accepted yet)
- `requested_token` with the issuance request for the token as json object (called for the token required to receive the
credentials)
Credential issuance (called for every credential that's issued (= requested from wallet))
- `jwt_issue` with `jwt` being the issued jwt
- `sdjwt_issue` with `sdjwt` being the issued sdjwt
- `batch_jwt_issue` with `jwt` being the issued jwt
- `batch_sdjwt_issue` with `sdjwt` being the issued sdjwt
- `generated_mdoc` with `mdoc` being the CBOR (HEX) of the signed mdoc
To allow for secure business logic flows, if a callback URL is set, and it cannot be reached, the issuance will not
commence further (after that point). If no callback URL is set, the issuance logic does not change in any way.
**Body**
As you can see for the property `issuerKey` we only provided the required parameters of the TSE Key Reference Object
described above and left out `_publicKey` and `_keyType`
```json
{
"issuerKey": {
"type": "tse",
"server": "https://vault.walt.id/v1/transit",
"auth": {
"roleId": "b50427bd-5070-0bec-3a00-dfe470ba349d",
"secretId": "22ffc0f4-c308-e6d6-f5a4-246cd89bc17c"
},
"id": "k-1481938705"
},
"issuerDid": "did:key:z6MkjoRhq1jSNJdLiruSXrFFxagqrztZaXHqHGUTKJbcNywp",
"credentialData": {
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://purl.imsglobal.org/spec/ob/v3p0/context.json"
],
"id": "urn:uuid:THIS WILL BE REPLACED WITH DYNAMIC DATA FUNCTION (see below)",
"type": [
"VerifiableCredential",
"OpenBadgeCredential"
],
"name": "JFF x vc-edu PlugFest 3 Interoperability",
...
},
"mapping": {
"id": "",
...
},
"standardVersion": "DRAFT13"
}
```
--------------------------------------------------------------------------------
PAGE: OCI Vault
URL: https://docs.walt.id/community-stack/issuer/key-management/oci-vault
RAW: https://docs.walt.id/community-stack/issuer/key-management/oci-vault.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# OCI Vault
OCI Vault (Oracle Cloud Infrastructure Vault) is a service that allows you to store and manage sensitive information such as keys, passwords, and
certificates. It provides a secure and centralized location to store and manage sensitive information, and it provides
access controls to ensure that only authorized users and applications can access the information.
The walt.id Wallet API provides an interface to interact with OCI Vault (Oracle Cloud Infrastructure Vault) to store and manage keys .
## OCI Vault (Oracle Cloud Infrastructure Vault) Configuration
On the Oracle Cloud Infrastructure (OCI) console, you can create a vault and manage the keys in the vault.
### Create a OCI Vault
To create a vault, you can follow the steps below:
1. Go to the OCI console and navigate to the Vault service.
2. Click on the `Create Vault` button.
3. Enter the details for the vault, such as the compartment and name.
4. Click on the `Create` button to create the vault.
5. Once the vault is created, you can manage the keys in the vault.

## Get OCI Vault Details
To get the details of the vault, you can follow the steps below:
1. Go to the OCI console and navigate to the Vault service.
2. Click on the vault to view the details of the vault.
3. You can view the details of the vault, such as the OCID, management endpoint, and crypto endpoint.
4. You can use this information to configure the wallet to use OCI Vault.

## Get tenancy OCID
To get the tenancy OCID, you can follow the steps below:
1. Go to the OCI console and click on the user profile.
2. Click on the `Tenancy:{name-of-tenancy}` to view the details of the tenancy.
3. You can view the details of the tenancy, such as the OCID.
4. You can use this OCID to configure the wallet to use OCI Vault.

## Get user OCID
To get the user OCID, you can follow the steps below:
1. Go to the OCI console and click on the user profile -> My profile.
2. You can view the details of the user, such as the OCID.
3. You can use this OCID to configure the wallet to use OCI Vault.

## Add Vault user
To use the vault api you need to add a user and provide him with the necessary permissions to access the vault.
Once added you can generate a private key for the user and use it to authenticate the wallet to the vault.
You can find more information on how to add a user and generate a fingerprint + private
key [here](https://docs.oracle.com/en-us/iaas/Content/API/Concepts/apisigningkey.htm)
## Instance Principal Authentication
To use oci sdk method you need to configure the vault for instance principal authentication. This is done by adding the
instance principal to the vault and providing the necessary permissions.
You can find more information on how to configure instance principal authentication [here](https://docs.oracle.com/en-us/iaas/Content/Identity/Tasks/callingservicesfrominstances.htm)
## Key Creation
Below you find an example call to create a key in OCI Vault and associate it with a wallet managed by the Wallet API.
The only thing that is important to note for the creation, that our system is only compatible with the following Key
types offered by the OCI Vault:
- RSA & ECDSA
Oracle KMS key
types [full list](https://docs.oracle.com/en-us/iaas/Content/KeyManagement/Tasks/dedicated_kms_pkcs_key_types.htm).
**Option: OCI SDK**
**Endpoint:
** `/onboard/issuer` | [API Reference](https://wallet.demo.walt.id/swagger/index.html#/Keys/post_wallet_api_wallet__wallet__keys_generate)
**Example Request**
```bash
curl -X 'POST' \
'https://wallet.demo.walt.id/wallet-api/wallet/f01f8f55-d098-4c53-b47b-c97552829b39/keys/generate' \
-H 'accept: */*' \
-H 'Content-Type: application/json' \
-d '{
"backend": "oci",
"config": {
"vaultId": "ocid1.vault.oc1.eu-frankfurt-1.entbf645aabf2.abtheljshkb6dsuldqf324kitneb63vkz3dfd74dtqvkd5j2l2cxwyvmefeq",
"compartmentId": "ocid1.compartment.oc1..aaaaaaaawirugoz35riiybcxsvf7bmelqsxo3sajaav5w3i2vqowcwqrllxa"
},
"keyType": "secp256r1"
}'
```
**Body**
```json
{
"backend": "oci",
"config": {
"vaultId": "ocid1.vault.oc1.eu-frankfurt-1.entbf645aabf2.abtheljshkb6dsuldqf324kitneb63vkz3dfd74dtqvkd5j2l2cxwyvmefeq",
"compartmentId": "ocid1.compartment.oc1..aaaaaaaawirugoz35riiybcxsvf7bmelqsxo3sajaav5w3i2vqowcwqrllxa"
},
"keyType": "secp256r1"
}
```
**Body Parameters**
- `backend`: _String_ - The location where the key is stored. In our case `oci` as we want to store it in oracle's
vault.
- `config`
- `vaultId`: _String_ - the OCID of the vault where the key should be stored.
- `compartmentId`: _String_ - the OCID of the compartment where the key should be stored.
- `keyType`: _String_ - the algorithm used to generate the key. For Vault only RSA and secp256r1 is possible.
---
**Example Response**
The API will respond with the ID of the key. This ID is the internal reference and can be used in operations such as DID
create or key delete.
```
Kki22j4lUwo1gtDfdvdCgOE0hhKcNHgIZSzSxU0CugE
```
**Option: OCI REST API**
**Endpoint:
** `/onboard/issuer` | [API Reference](https://wallet.demo.walt.id/swagger/index.html#/Keys/post_wallet_api_wallet__wallet__keys_generate)
**Example Request**
```bash
curl -X 'POST' \
'https://wallet.demo.walt.id/wallet-api/wallet/f01f8f55-d098-4c53-b47b-c97552829b39/keys/generate' \
-H 'accept: */*' \
-H 'Content-Type: application/json' \
-d '{
"backend": "oci-rest-api",
"config": {
"tenancyOcid": "ocid1.tenancy.oc1..aaaaaaaaxxxxxx",
"compartmentOcid": "ocid1.tenancy.oc1..aaaaaaaaxxxxxx",
"userOcid": "ocid1.user.oc1..aaaaaaaaxxxxxx",
"fingerprint": "d4:4b:0c:c8:3a:49:15:7f:87",
"managementEndpoint": "vault-management.region.oraclecloud.com",
"cryptoEndpoint": "vault-crypto.region.oraclecloud.com",
"signingKeyPem": "-----BEGIN PRIVATE KEY-----"
},
"keyType": "secp256r1"
}'
```
**Body**
```json
{
"backend": "oci-rest-api",
"config": {
"tenancyOcid": "ocid1.tenancy.oc1..aaaaaaaaxxxxxx",
"compartmentOcid": "ocid1.tenancy.oc1..aaaaaaaaxxxxxx",
"userOcid": "ocid1.user.oc1..aaaaaaaaxxxxxx",
"fingerprint": "d4:4b:0c:c8:3a:49:15:7f:87",
"managementEndpoint": "vault-management.region.oraclecloud.com",
"cryptoEndpoint": "vault-crypto.region.oraclecloud.com",
"signingKeyPem": "-----BEGIN PRIVATE KEY-----"
},
"keyType": "secp256r1"
}
```
**Body Parameters**
- `backend`: _String_ - The location where the key is stored. In our case `oci-rest-api` as we want to store it in oracle's
vault.
- `config`
- `tenancyOcid`: _String_ - The OCID of the tenancy.
- `compartmentOcid`: _String_ - The OCID of the compartment.
- `userOcid`: _String_ - The OCID of the user.
- `fingerprint`: _String_ - The fingerprint of the user's public key.
- `managementEndpoint`: _URL_ - The management endpoint of the vault.
- `cryptoEndpoint`: _URL_ - The crypto endpoint of the vault.
- `signingKeyPem`: _String_ - The private key of the user.
- `keyType`: _String_ - the algorithm used to generate the key. For Vault only RSA and secp256r1 is possible.
---
**Example Response**
The API will respond with the ID of the key. This ID is the internal reference and can be used in operations such as DID
create or key delete.
```
Kki22j4lUwo1gtDfdvdCgOE0hhKcNHgIZSzSxU0CugE
```
### Creation via Issuer API
**Option: OCI SDK**
**Endpoint:
** `/onboard/issuer` | [API Reference](https://issuer.demo.walt.id/swagger/index.html#/Issuer%20onboarding/post_onboard_issuer)
**Example Request**
```bash
curl -X 'POST' \
'https://issuer.demo.walt.id/onboard/issuer' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"key": {
"backend": "oci",
"keyType": "secp256r1",
"config": {
"tenancyOcid": "ocid1.tenancy.oc1..aaaaaaaaiijfupfvsqwqwgupzdy5yclfzcccmie4ktp2wlgslftv5j7xpk6q",
"compartmentOcid": "ocid1.tenancy.oc1..aaaaaaaaiijfupfvsqwqwgupzdy5yclfzcccmie4ktp2wlgslftv5j7xpk6q",
"userOcid": "ocid1.user.oc1..aaaaaaaaxjkkfjqxdqk7ldfjrxjmacmbi7sci73rbfiwpioehikavpbtqx5q",
"fingerprint": "bb:d4:4b:0c:c8:3a:49:15:7f:87:55:d5:2b:7e:dd:bc",
"managementEndpoint": "entaftlvaaemy-management.kms.eu-frankfurt-1.oraclecloud.com",
"cryptoEndpoint": "entaftlvaaemy-crypto.kms.eu-frankfurt-1.oraclecloud.com",
"signingKeyPem": "-----BEGIN PRIVATE KEY-----
-----END PRIVATE KEY-----
"
}
},
"did": {
"method": "jwk"
}
}'
```
**Body**
```json
{
"key": {
"backend": "oci",
"keyType": "secp256r1",
"config": {
"tenancyOcid": "ocid1.tenancy.oc1..aaaaaaaaiijfupfvsqwqwgupzdy5yclfzcccmie4ktp2wlgslftv5j7xpk6q",
"compartmentOcid": "ocid1.tenancy.oc1..aaaaaaaaiijfupfvsqwqwgupzdy5yclfzcccmie4ktp2wlgslftv5j7xpk6q",
"userOcid": "ocid1.user.oc1..aaaaaaaaxjkkfjqxdqk7ldfjrxjmacmbi7sci73rbfiwpioehikavpbtqx5q",
"fingerprint": "bb:d4:4b:0c:c8:3a:49:15:7f:87:55:d5:2b:7e:dd:bc",
"managementEndpoint": "entaftlvaaemy-management.kms.eu-frankfurt-1.oraclecloud.com",
"cryptoEndpoint": "entaftlvaaemy-crypto.kms.eu-frankfurt-1.oraclecloud.com",
"signingKeyPem": "-----BEGIN PRIVATE KEY-----
-----END PRIVATE KEY-----
"
}
},
"did": {
"method": "jwk"
}
}
```
**Body Parameters**
- `key`
- `backend`: _String_ - Specifies the storage type of key. It can be `jwk` (manged by you), `TSE` (managed by
Hashicorp) or `oci` (managed by Oracle)
- `keyType`: _String_ - the algorithm used to generate the key. For Vault only secp256r1 is possible.
- `did`:
- `method`: _String_ - Specifies the DID method. It can be key, jwk, web, cheqd.
---
**Example Response**
The onboard/issuer endpoint will return an object containing both the generated key in JWK format and the related DID.
```json
{
"issuerKey": {
"type": "oci",
"config": {
"vaultId": "ocid1.vault.oc1.eu-frankfurt-1.entbf645aabf2.abtheljshkb6dsuldqf324kitneb63vkz3dfd74dtqvkd5j2l2cxwyvmefeq",
"compartmentId": "ocid1.compartment.oc1..aaaaaaaawirugoz35riiybcxsvf7bmelqsxo3sajaav5w3i2vqowcwqrllxa"
},
"id": "ocid1.key.oc1.eu-frankfurt-1.enta2fneaadmk.abtheljrlj5snthwkx7ycdmknuftght527dkyjsoz72dcogklixrsdyolo5a",
"_publicKey": "{\"kty\":\"EC\",\"crv\":\"P-256\",\"x\":\"tT1DAZdtp7vUPphTxoilmr6dfZPKcPfwL8G_Ri3K0_E\",\"y\":\"JabPubkHQPK0G7O8eL3bKg75hX4Wkojb_AOepX8xdAs\"}",
"_keyType": "secp256r1"
},
"issuerDid": "did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6InRUMURBWmR0cDd2VVBwaFR4b2lsbXI2ZGZaUEtjUGZ3TDhHX1JpM0swX0UiLCJ5IjoiSmFiUHVia0hRUEswRzdPOGVMM2JLZzc1aFg0V2tvamJfQU9lcFg4eGRBcyJ9"
}
```
**Properties**
- `type`: _String_ - the type of key can be either "oci" when using OCI Vault or "jwk" when providing the key in
full as JWK.
- `config`: _Object_ - the configuration object containing the necessary information to access the OCI Vault.
- `id`: _String_ - the ID of the key in the OCI Vault.
- `_publicKey` (optional): _Array_ - The public key can be fetched by the issuer API or directly provided, saving
resources and reducing network requests.
- `_keyType` (optional): _String_ - The key type can be fetched by the issuer API or directly provided, saving resources
and reducing network requests.
**Option: OCI REST API**
**Endpoint:
** `/onboard/issuer` | [API Reference](https://issuer.demo.walt.id/swagger/index.html#/Issuer%20onboarding/post_onboard_issuer)
**Example Request**
```bash
curl -X 'POST' \
'https://issuer.demo.walt.id/onboard/issuer' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"issuanceKeyConfig": {
"type": "oci-rest-api",
"tenancyOcid": "ocid1.tenancy.oc1..aaaaaaaaxxxxxx",
"compartmentOcid": "ocid1.tenancy.oc1..aaaaaaaaxxxxxx",
"userOcid": "ocid1.user.oc1..aaaaaaaaxxxxxx",
"fingerprint": "d4:4b:0c:c8:3a:49:15:7f:87",
"managementEndpoint": "vault-management.region.oraclecloud.com",
"cryptoEndpoint": "vault-crypto.region.oraclecloud.com",
"signingKeyPem": "-----BEGIN PRIVATE KEY-----",
"algorithm": "secp256r1"
},
"issuerDidConfig": {
"method": "jwk"
}
}'
```
**Body**
```json
{
"issuanceKeyConfig": {
"type": "oci-rest-api",
"tenancyOcid": "ocid1.tenancy.oc1..aaaaaaaaxxxxxx",
"compartmentOcid": "ocid1.tenancy.oc1..aaaaaaaaxxxxxx",
"userOcid": "ocid1.user.oc1..aaaaaaaaxxxxxx",
"fingerprint": "d4:4b:0c:c8:3a:49:15:7f:87",
"managementEndpoint": "vault-management.region.oraclecloud.com",
"cryptoEndpoint": "vault-crypto.region.oraclecloud.com",
"signingKeyPem": "-----BEGIN PRIVATE KEY-----",
"algorithm": "secp256r1"
},
"issuerDidConfig": {
"method": "key"
}
}
```
**Body Parameters**
- `issuanceKeyConfig`
- `type`: _String_ - Specifies the storage type of key. It can be `jwk` (manged by you), `TSE` (managed by
Hashicorp) or `oci` (managed by Oracle)
- `algorithm`: _String_ - the algorithm used to generate the key. For Vault only secp256r1 is possible.
- `issuerDidConfig`:
- `method`: _String_ - Specifies the DID method. It can be key, jwk, web, cheqd.
---
**Example Response**
The onboard/issuer endpoint will return an object containing both the generated key in JWK format and the related DID.
```json
{
"issuanceKey": {
"type": "oci-rest-api",
"config": {
"tenancyOcid": "ocid1.tenancy.oc1..aaaaaaaaxxxxxx",
"compartmentOcid": "ocid1.tenancy.oc1..aaaaaaaaxxxxxx",
"userOcid": "ocid1.user.oc1..aaaaaaaaxxxxxx",
"fingerprint": "d4:4b:0c:c8:3a:49:15:7f:87",
"managementEndpoint": "vault-management.region.oraclecloud.com",
"cryptoEndpoint": "vault-crypto.region.oraclecloud.com",
"signingKeyPem": "-----BEGIN PRIVATE KEY-----"
},
"id": "ocid1.key.oc1.eu-frankfurt-1.enta2fneaadmk.abtheljrlj5snthwkx7ycdmknuftght527dkyjsoz72dcogklixrsdyolo5a",
"_publicKey": "{\"kty\":\"EC\",\"crv\":\"P-256\",\"x\":\"tT1DAZdtp7vUPphTxoilmr6dfZPKcPfwL8G_Ri3K0_E\",\"y\":\"JabPubkHQPK0G7O8eL3bKg75hX4Wkojb_AOepX8xdAs\"}",
"_keyType": "secp256r1"
},
"issuerDid": "did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6InRUMURBWmR0cDd2VVBwaFR4b2lsbXI2ZGZaUEtjUGZ3TDhHX1JpM0swX0UiLCJ5IjoiSmFiUHVia0hRUEswRzdPOGVMM2JLZzc1aFg0V2tvamJfQU9lcFg4eGRBcyJ9"
}
```
**Properties**
- `type`: _String_ - the type of key can be either "oci" when using OCI Vault or "jwk" when providing the key in
full as JWK.
- `config`: _Object_ - the configuration object containing the necessary information to access the OCI Vault.
- `id`: _String_ - the ID of the key in the OCI Vault.
- `_publicKey` (optional): _Array_ - The public key can be fetched by the issuer API or directly provided, saving
resources and reducing network requests.
- `_keyType` (optional): _String_ - The key type can be fetched by the issuer API or directly provided, saving resources
and reducing network requests.
## Key Usage
Once you have successfully created a key that is one of the supported types listed above, you can use it in
sign and issue operations offered by the issuer API.
If you've already had a look at
our `/sign`, `/issue`, `/batchIssue`
endpoints, you have seen that they all follow a similar request body structure, where the key that should be
used for signing credentials is provided via the `issuerKey` property. Now instead of providing the key as JWK, we
provide
a reference to a key stored in Vault with the required parameters and access credentials.
Below you can see an example of
`issuerKey` object referencing a key stored in Vault.
**Option: Example IssuerKey Object**
```json
{
"type": "oci-rest-api",
"config": {
"tenancyOcid": "ocid1.tenancy.oc1..aaaaaaaaxxxxxx",
"compartmentOcid": "ocid1.tenancy.oc1..aaaaaaaaxxxxxx",
"userOcid": "ocid1.user.oc1..aaaaaaaaxxxxxx",
"fingerprint": "d4:4b:0c:c8:3a:49:15:7f:87",
"managementEndpoint": "vault-management.region.oraclecloud.com",
"cryptoEndpoint": "vault-crypto.region.oraclecloud.com",
"signingKeyPem": "-----BEGIN PRIVATE KEY-----"
},
"id": "ocid1.key.oc1.eu-frankfurt-1.entaftlvaaemy.abtheljtkwxupytaepjrgvgbhoufoonh2kbevc3cenjetbpeabhgcxyzcmwq",
"_publicKey": "{\"kty\":\"EC\",\"crv\":\"P-256\",\"x\":\"r1FURkMEYWiYL_IKi1bG0Ar-N36Z5bvocBcEKtkGF_s\",\"y\":\"0QP3QWwVXfpZf_i-T9qOaSMfd3_or7t6JtbJr9YkvPY\"}",
"_keyType": "secp256r1"
}
```
**Properties**
- `type`: _String_ - the type of key can be either "oci" when using OCI Vault or "jwk" when providing the key in full
as JWK.
- `config`: _Object_ - the configuration object containing the necessary information to access the OCI Vault.
- `id`: _String_ - the ID of the key in the OCI Vault.
- `_publicKey` (optional): _Array_ - The public key can be fetched by the issuer API or directly provided, saving
resources and reducing network requests.
- `_keyType` (optional): _String_ - The key type can be fetched by the issuer API or directly provided, saving resources
and reducing network requests.
## Example Issuance Request
Below you can see example issuance request
to [`jwt/issue`](https://issuer.demo.walt.id/swagger/index.html#/Credential%20Issuance/post_openid4vc_jwt_issue) using
a key created in the Vault Transit Secret Engine to
sign the credential.
**Option: OCI SDK**
[Api Reference](https://issuer.demo.walt.id/swagger/index.html#/Credential%20Issuance/post_openid4vc_jwt_issue)
```bash
curl -X 'POST' \
'https://issuer.demo.walt.id/openid4vc/jwt/issue' \
-H 'accept: text/plain' \
-H 'Content-Type: application/json' \
-d '{
"issuerKey": {
"type": "oci",
"config": {
"vaultId": "ocid1.vault.oc1.eu-frankfurt-1.entbf645aabf2.abtheljshkb6dsuldqf324kitneb63vkz3dfd74dtqvkd5j2l2cxwyvmefeq",
"compartmentId": "ocid1.compartment.oc1..aaaaaaaawirugoz35riiybcxsvf7bmelqsxo3sajaav5w3i2vqowcwqrllxa"
},
"id": "ocid1.key.oc1.eu-frankfurt-1.entaftlvaaemy.abtheljtkwxupytaepjrgvgbhoufoonh2kbevc3cenjetbpeabhgcxyzcmwq",
"_publicKey": "{\"kty\":\"EC\",\"crv\":\"P-256\",\"x\":\"r1FURkMEYWiYL_IKi1bG0Ar-N36Z5bvocBcEKtkGF_s\",\"y\":\"0QP3QWwVXfpZf_i-T9qOaSMfd3_or7t6JtbJr9YkvPY\"}",
"_keyType": "secp256r1"
},
"issuerDid": "did:key:z6MkjoRhq1jSNJdLiruSXrFFxagqrztZaXHqHGUTKJbcNywp",
"credentialData": {
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://purl.imsglobal.org/spec/ob/v3p0/context.json"
],
"id": "urn:uuid:THIS WILL BE REPLACED WITH DYNAMIC DATA FUNCTION (see below)",
"type": [
"VerifiableCredential",
"OpenBadgeCredential"
],
"name": "JFF x vc-edu PlugFest 3 Interoperability",
...
},
"mapping": {
"id": "",
...
},
"standardVersion": "DRAFT13"
}'
```
**Body**
As you can see for the property `issuanceKey` we only provided the required parameters of the OCI Key Reference Object
described above and left out `_publicKey` and `_keyType`
```json
{
"issuanceKey": {
"type": "oci",
"config": {
"vaultId": "ocid1.vault.oc1.eu-frankfurt-1.entbf645aabf2.abtheljshkb6dsuldqf324kitneb63vkz3dfd74dtqvkd5j2l2cxwyvmefeq",
"compartmentId": "ocid1.compartment.oc1..aaaaaaaawirugoz35riiybcxsvf7bmelqsxo3sajaav5w3i2vqowcwqrllxa"
},
"id": "ocid1.key.oc1.eu-frankfurt-1.entaftlvaaemy.abtheljtkwxupytaepjrgvgbhoufoonh2kbevc3cenjetbpeabhgcxyzcmwq",
"_publicKey": "{\"kty\":\"EC\",\"crv\":\"P-256\",\"x\":\"r1FURkMEYWiYL_IKi1bG0Ar-N36Z5bvocBcEKtkGF_s\",\"y\":\"0QP3QWwVXfpZf_i-T9qOaSMfd3_or7t6JtbJr9YkvPY\"}",
"_keyType": "secp256r1"
},
"issuerDid": "did:key:z6MkjoRhq1jSNJdLiruSXrFFxagqrztZaXHqHGUTKJbcNywp",
"credentialData": {
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://purl.imsglobal.org/spec/ob/v3p0/context.json"
],
"id": "urn:uuid:THIS WILL BE REPLACED WITH DYNAMIC DATA FUNCTION (see below)",
"type": [
"VerifiableCredential",
"OpenBadgeCredential"
],
"name": "JFF x vc-edu PlugFest 3 Interoperability",
...
},
"mapping": {
"id": "",
...
},
"standardVersion": "DRAFT13"
}
```
**Option: OCI REST API**
[Api Reference](https://issuer.demo.walt.id/swagger/index.html#/Credential%20Issuance/post_openid4vc_jwt_issue)
```bash
curl -X 'POST' \
'https://issuer.demo.walt.id/openid4vc/jwt/issue' \
-H 'accept: text/plain' \
-H 'statusCallbackUri: https://example.com/$id' \
-H 'Content-Type: application/json' \
-d '{
"issuanceKey": {
"type": "oci-rest-api",
"config": {
"tenancyOcid": "ocid1.tenancy.oc1..aaaaaaaaiijfupfvsqwqwgupzdy5yclfzcccmie4ktp2wlgslftv5j7xpk6q",
"compartmentOcid": "ocid1.compartment.oc1..aaaaaaaaxjkkfjqxdqk7ldfjrxjmacmbi7sci73rbfiwpioehikavpbtqx5q",
"userOcid": "ocid1.user.oc1..aaaaaaaaxjkkfjqxdqk7ldfjrxjmacmbi7sci73rbfiwpioehikavpbtqx5q",
"fingerprint": "bb:d4:4b:0c:c8:3a:49:15:7f:87:55:d5:2b:7e:dd:bc",
"cryptoEndpoint": "ens7pgl2aaam2-crypto.kms.eu-frankfurt-1.oraclecloud.com",
"managementEndpoint": "ens7pgl2aaam2-management.kms.eu-frankfurt-1.oraclecloud.com",
"signingKeyPem": "privateKey"
},
"id": "ocid1.key.oc1.eu-frankfurt-1.entaftlvaaemy.abtheljtkwxupytaepjrgvgbhoufoonh2kbevc3cenjetbpeabhgcxyzcmwq",
"_publicKey": "{\"kty\":\"EC\",\"crv\":\"P-256\",\"x\":\"r1FURkMEYWiYL_IKi1bG0Ar-N36Z5bvocBcEKtkGF_s\",\"y\":\"0QP3QWwVXfpZf_i-T9qOaSMfd3_or7t6JtbJr9YkvPY\"}",
"_keyType": "secp256r1"
},
"issuerDid": "did:key:z6MkjoRhq1jSNJdLiruSXrFFxagqrztZaXHqHGUTKJbcNywp",
"credentialData": {
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://purl.imsglobal.org/spec/ob/v3p0/context.json"
],
"id": "urn:uuid:THIS WILL BE REPLACED WITH DYNAMIC DATA FUNCTION (see below)",
"type": [
"VerifiableCredential",
"OpenBadgeCredential"
],
"name": "JFF x vc-edu PlugFest 3 Interoperability",
...
},
"mapping": {
"id": "",
...
},
"standardVersion": "DRAFT13"
}'
```
**Header Parameters**
- `statusCallbackUri`: _URL_ - Receive updates on the created issuance process, e.g. when a credential was successfully
claimed. The parameter expects a URL which can accept a JSON POST request. The URL can also hold a `$id`, which will
be
replaced by the issuance session id. For example: `https://myurl.com/$id`, `https://myurl.com`
or `https://myurl.com/test/$id`
Expand To Learn More
**Body**
The data send to the provided URL will contain a JSON body:
- `id` : _String_ - the issuance session id
- `type`: _String_ - the event type
- `data`: _JsonObject_ - the data for the event
**Event Types**
Possible events (event types) and their data are:
- `resolved_credential_offer` with the credential offer as JSON (in our Web Wallet: called when the issuance offer is
entered into the wallet, but not processing / accepted yet)
- `requested_token` with the issuance request for the token as json object (called for the token required to receive the
credentials)
Credential issuance (called for every credential that's issued (= requested from wallet))
- `jwt_issue` with `jwt` being the issued jwt
- `sdjwt_issue` with `sdjwt` being the issued sdjwt
- `batch_jwt_issue` with `jwt` being the issued jwt
- `batch_sdjwt_issue` with `sdjwt` being the issued sdjwt
- `generated_mdoc` with `mdoc` being the CBOR (HEX) of the signed mdoc
To allow for secure business logic flows, if a callback URL is set, and it cannot be reached, the issuance will not
commence further (after that point). If no callback URL is set, the issuance logic does not change in any way.
**Body**
As you can see for the property `issuerKey` we only provided the required parameters of the OCI Key Reference Object
described above and left out `_publicKey` and `_keyType`
```json
{
"issuerKey": {
"type": "oci",
"config": {
"tenancyOcid": "ocid1.tenancy.oc1..aaaaaaaaiijfupfvsqwqwgupzdy5yclfzcccmie4ktp2wlgslftv5j7xpk6q",
"compartmentOcid": "ocid1.compartment.oc1..aaaaaaaawirugoz35riiybcxsvf7bmelqsxo3sajaav5w3i2vqowcwqrllxa",
"userOcid": "ocid1.user.oc1..aaaaaaaaxjkkfjqxdqk7ldfjrxjmacmbi7sci73rbfiwpioehikavpbtqx5q",
"fingerprint": "bb:d4:4b:0c:c8:3a:49:15:7f:87:55:d5:2b:7e:dd:bc",
"managementEndpoint": "entaftlvaaemy-management.kms.eu-frankfurt-1.oraclecloud.com",
"cryptoEndpoint": "entaftlvaaemy-crypto.kms.eu-frankfurt-1.oraclecloud.com",
"signingKeyPem": "-----BEGIN PRIVATE KEY-----
-----END PRIVATE KEY-----
"
},
"id": "ocid1.key.oc1.eu-frankfurt-1.entaftlvaaemy.abtheljtkwxupytaepjrgvgbhoufoonh2kbevc3cenjetbpeabhgcxyzcmwq",
"_publicKey": "{\"kty\":\"EC\",\"crv\":\"P-256\",\"x\":\"r1FURkMEYWiYL_IKi1bG0Ar-N36Z5bvocBcEKtkGF_s\",\"y\":\"0QP3QWwVXfpZf_i-T9qOaSMfd3_or7t6JtbJr9YkvPY\"}",
"_keyType": "secp256r1"
},
"issuerDid": "did:key:z6MkjoRhq1jSNJdLiruSXrFFxagqrztZaXHqHGUTKJbcNywp",
"credentialData": {
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://purl.imsglobal.org/spec/ob/v3p0/context.json"
],
"id": "urn:uuid:THIS WILL BE REPLACED WITH DYNAMIC DATA FUNCTION (see below)",
"type": [
"VerifiableCredential",
"OpenBadgeCredential"
],
"name": "JFF x vc-edu PlugFest 3 Interoperability",
...
},
"mapping": {
"id": "",
...
},
"standardVersion": "DRAFT13"
}
```
--------------------------------------------------------------------------------
PAGE: Overview
URL: https://docs.walt.id/community-stack/issuer/key-management/aws-kms/overview
RAW: https://docs.walt.id/community-stack/issuer/key-management/aws-kms/overview.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Overview
The issuer API support AWS KMS as a key management solution. This allows you to store your cryptographic keys in a secure and scalable way. The API is designed to be stateless and does not store cryptographic key material. Therefore, every API call requires the cryptographic key to be provided. When signing and issuing a credential e.g. via [**OpenID for Verifiable Credential Issuance (OID4VCI)**](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md), either the complete key in JWK format or a reference to a key stored in an external KMS solution is needed. In production systems, the recommended approach is to use an external KMS solution, so that the private key used to sign a credential never leaves a secure environment.
We support the following key types offered by the AWS KMS:
- RSA
- SECP256R1
- SECP256K1
We have created a Kotlin-based extension that enhances walt.id crypto with native AWS key management capabilities. This extension introduces `AwsKey`, a robust implementation leveraging the AWS SDK for Kotlin to manage cryptographic keys. `AwsKey` serves as a more integrated alternative to the platform-agnostic `AWSKeyRestAPI` found in the base walt.id crypto library.
## Key Features
- Native AWS SDK integration for optimal performance
- Kotlin-specific implementation
- Seamless key management through AWS KMS
## Authentication
The extension utilizes AWS SDK's default credential provider chain for authentication, automatically detecting credentials from multiple sources including:
- Environment variables
- AWS credentials file
- IAM roles for EC2
- Container credentials
- SSO credentials
## Comparison to Base Implementation
While the base `AWSKeyRestAPI` offers cross-platform compatibility through REST endpoints, this extension provides:
- Improved performance through direct SDK calls
- Enhanced error handling
- Native integration with AWS services
## Get Started
- [AWS KMS SDK](https://docs.walt.id/community-stack/issuer/key-management/aws-kms/sdk.md) - Use keys from AWS KMS to sign credentials
- [AWS KMS REST API](https://docs.walt.id/community-stack/issuer/key-management/aws-kms/rest-api.md) - Use keys from AWS KMS Rest Api to sign credentials
--------------------------------------------------------------------------------
PAGE: SDK
URL: https://docs.walt.id/community-stack/issuer/key-management/aws-kms/sdk
RAW: https://docs.walt.id/community-stack/issuer/key-management/aws-kms/sdk.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# SDK
This implementation utilizes AWS SDK's default credential provider chain for authentication, automatically detecting
credentials from multiple sources including:
- Environment variables
- AWS credentials file
- IAM roles for EC2
- Container credentials
- SSO credentials
### Key generation
To create the key you can use the onboard endpoint provided by the issuer API and provide the necessary parameters
to create the key in the AWS KMS service.
#### Creation via Issuer API
**Option: Request**
**Endpoint:**
`/onboard/issuer` | [API Reference](https://issuer.demo.walt.id/swagger/index.html#/Issuer%20onboarding/post_onboard_issuer)
**Example Request**
```bash
curl -X 'POST' \
'https://issuer.demo.walt.id/onboard/issuer' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"key": {
"backend": "aws",
"keyType": "secp256r1",
"config": {
"auth": {
"region": "eu-central-1",
},
"tags": {
"key": "value"
}
}
},
"did": {
"method": "jwk"
}
}'
```
**Body**
```json
{
"key": {
"backend": "aws",
"keyType": "secp256r1",
"config": {
"auth": {
"region": "eu-central-1",
},
"tags": {
"key": "value"
}
}
},
"did": {
"method": "jwk"
}
}
```
**Body Parameters**
- `key`
- `backend`: _String_ - Specifies the storage type of key. It can be `jwk` (managed by you), `aws` (managed
by
AWS KMS using their kotlin SDK ) and others. Learn more about different
types [here](https://docs.walt.id/community-stack/issuer/key-management/overview.md).
- `keyType`: _String_ - the algorithm used to generate the key. Supported AWS KMS values are `secp256r1`, `secp384r1`, `secp521r1`, `secp256k1`, `RSA`, `RSA3072`, and `RSA4096`. `Ed25519` is not supported by AWS KMS signing keys.
- `config`
- `auth`
- `region`: _String_ - The region where the AWS KMS service is located.
- `tags`: _Object_ - The metadata tags to add to the key for [AWS](https://docs.aws.amazon.com/kms/latest/developerguide/tagging-keys.html)
- `did`:
- `method`: _String_ - Specifies the DID method. It can be key, jwk, web, cheqd.
**Option: Response**
**Example Response**
The onboard/issuer endpoint will return an object containing both the generated key in JWK format and the related DID.
```json
{
"issuerKey": {
"type": "aws",
"config": {
"auth": {
"region": "eu-central-1"
}
},
"id": "324ebf67-6bcc-4439-8b81-260bf0a82532",
"_publicKey": "{\"kty\":\"EC\",\"crv\":\"P-256\",\"x\":\"RfvJGDXOjz3NOSKgZ0VlijXST8S-j96DrG0C1AMNAK8\",\"y\":\"vSt2q7yIy0-AhAirMuuDmUScxkgf4JVfId-vTETraQA\"}",
"_keyType": "secp256r1"
},
"issuerDid": "did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IlJmdkpHRFhPanozTk9TS2daMFZsaWpYU1Q4Uy1qOTZEckcwQzFBTU5BSzgiLCJ5IjoidlN0MnE3eUl5MC1BaEFpck11dURtVVNjeGtnZjRKVmZJZC12VEVUcmFRQSJ9"
}
```
**Properties**
- `type`: _String_ - the type of key can be either "aws" when using AWS KMS or "jwk" when providing the key in
full as JWK.
- `config`
- `auth`
- `region`: _String_ - The region where the AWS KMS service is located.
- `id`: _String_ - the ID of the key in the Transit Engine.
- `_publicKey` (optional): _Array_ - The public key can be fetched by the issuer API or directly provided, saving
resources and reducing network requests.
- `_keyType` (optional): _String_ - The key type can be fetched by the issuer API or directly provided, saving resources
and reducing network requests.
- `issuerDid`: _String_ - the DID of the issuer.
### Key Usage
Once you have successfully created a key that is one of the supported types listed above, you can use it in
sign and issue operations offered by the issuer API.
If you've already had a look at
our `/sign`, `/issue`, `/batchIssue`
endpoints, you have seen that they all follow a similar request body structure, where the key that should be
used for signing credentials is provided via the `issuerKey` property. Now instead of providing the key as JWK, we
provide
a reference to a key stored in Vault with the required parameters and access credentials.
Below you can see an example of
`issuerKey` object referencing a key stored in AWS.
**Option: Example IssuerKey Object**
```json
{
"issuerKey": {
"type": "aws",
"config": {
"auth": {
"region": "eu-central-1",
},
"tags": {
"key": "value"
}
},
"id": "324ebf67-6bcc-4439-8b81-260bf0a82532",
"_publicKey": "{\"kty\":\"EC\",\"crv\":\"P-256\",\"x\":\"RfvJGDXOjz3NOSKgZ0VlijXST8S-j96DrG0C1AMNAK8\",\"y\":\"vSt2q7yIy0-AhAirMuuDmUScxkgf4JVfId-vTETraQA\"}",
"_keyType": "secp256r1"
},
"issuerDid": "did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IlJmdkpHRFhPanozTk9TS2daMFZsaWpYU1Q4Uy1qOTZEckcwQzFBTU5BSzgiLCJ5IjoidlN0MnE3eUl5MC1BaEFpck11dURtVVNjeGtnZjRKVmZJZC12VEVUcmFRQSJ9"
}
```
**Properties**
- `type`: _String_ - the type of key can be either "aws" when using AWS KMS or "jwk" when providing the key in
full as JWK.
-
- `id`: _String_ - the ID of the key in the Transit Engine.
- `config`
- `auth`
- `region`: _String_ - The region where the AWS KMS service is located.
- `_publicKey` (optional): _Array_ - The public key can be fetched by the issuer API or directly provided, saving
resources and reducing network requests.
- `_keyType` (optional): _String_ - The key type can be fetched by the issuer API or directly provided, saving resources
and reducing network requests.
**Option: Example Issuance Request**
## Example Issuance Request
Below you can see example issuance request
to [`jwt/issue`](https://issuer.demo.walt.id/swagger/index.html#/Credential%20Issuance/post_openid4vc_jwt_issue) using
a key created in AWS kms to
sign the credential.
[Api Reference](https://issuer.demo.walt.id/swagger/index.html#/Credential%20Issuance/post_openid4vc_jwt_issue)
```bash
curl -X 'POST' \
'https://issuer.demo.walt.id/openid4vc/jwt/issue' \
-H 'accept: text/plain' \
-H 'statusCallbackUri: https://example.com/$id' \
-H 'Content-Type: application/json' \
-d '{
"issuerKey": {
"type": "aws",
"config": {
"auth": {
"region": "eu-central-1",
},
"tags": {
"key": "value"
}
},
"id": "324ebf67-6bcc-4439-8b81-260bf0a82532",
"_publicKey": "{\"kty\":\"EC\",\"crv\":\"P-256\",\"x\":\"RfvJGDXOjz3NOSKgZ0VlijXST8S-j96DrG0C1AMNAK8\",\"y\":\"vSt2q7yIy0-AhAirMuuDmUScxkgf4JVfId-vTETraQA\"}",
"_keyType": "secp256r1"
},
"issuerDid": "did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IlJmdkpHRFhPanozTk9TS2daMFZsaWpYU1Q4Uy1qOTZEckcwQzFBTU5BSzgiLCJ5IjoidlN0MnE3eUl5MC1BaEFpck11dURtVVNjeGtnZjRKVmZJZC12VEVUcmFRQSJ9",
"credentialData": {
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://purl.imsglobal.org/spec/ob/v3p0/context.json"
],
"id": "urn:uuid:THIS WILL BE REPLACED WITH DYNAMIC DATA FUNCTION (see below)",
"type": [
"VerifiableCredential",
"OpenBadgeCredential"
],
"name": "JFF x vc-edu PlugFest 3 Interoperability",
...
},
"mapping": {
"id": "",
...
}
}'
```
**Header Parameters**
- `statusCallbackUri`: _URL_ - Receive updates on the created issuance process, e.g. when a credential was successfully
claimed. The parameter expects a URL which can accept a JSON POST request. The URL can also hold a `$id`, which will
be
replaced by the issuance session id. For example: `https://myurl.com/$id`, `https://myurl.com`
or `https://myurl.com/test/$id`
Expand To Learn More
**Body**
The data send to the provided URL will contain a JSON body:
- `id` : _String_ - the issuance session id
- `type`: _String_ - the event type
- `data`: _JsonObject_ - the data for the event
**Event Types**
Possible events (event types) and their data are:
- `resolved_credential_offer` with the credential offer as JSON (in our Web Wallet: called when the issuance offer
is
entered into the wallet, but not processing / accepted yet)
- `requested_token` with the issuance request for the token as json object (called for the token required to receive
the
credentials)
Credential issuance (called for every credential that's issued (= requested from wallet))
- `jwt_issue` with `jwt` being the issued jwt
- `sdjwt_issue` with `sdjwt` being the issued sdjwt
- `batch_jwt_issue` with `jwt` being the issued jwt
- `batch_sdjwt_issue` with `sdjwt` being the issued sdjwt
- `generated_mdoc` with `mdoc` being the CBOR (HEX) of the signed mdoc
To allow for secure business logic flows, if a callback URL is set, and it cannot be reached, the issuance will not
commence further (after that point). If no callback URL is set, the issuance logic does not change in any way.
**Body**
As you can see for the property `issuerKey` we only provided the required parameters of the TSE Key Reference Object
described above and left out `_publicKey` and `_keyType`
```json
{
"issuerKey": {
"type": "aws",
"config": {
"auth": {
"region": "eu-central-1",
},
"tags": {
"key": "value"
}
},
"id": "324ebf67-6bcc-4439-8b81-260bf0a82532",
"_publicKey": "{\"kty\":\"EC\",\"crv\":\"P-256\",\"x\":\"RfvJGDXOjz3NOSKgZ0VlijXST8S-j96DrG0C1AMNAK8\",\"y\":\"vSt2q7yIy0-AhAirMuuDmUScxkgf4JVfId-vTETraQA\"}",
"_keyType": "secp256r1"
},
"issuerDid": "did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IlJmdkpHRFhPanozTk9TS2daMFZsaWpYU1Q4Uy1qOTZEckcwQzFBTU5BSzgiLCJ5IjoidlN0MnE3eUl5MC1BaEFpck11dURtVVNjeGtnZjRKVmZJZC12VEVUcmFRQSJ9",
"credentialData": {
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://purl.imsglobal.org/spec/ob/v3p0/context.json"
],
"id": "urn:uuid:THIS WILL BE REPLACED WITH DYNAMIC DATA FUNCTION (see below)",
"type": [
"VerifiableCredential",
"OpenBadgeCredential"
],
"name": "JFF x vc-edu PlugFest 3 Interoperability",
...
},
"mapping": {
"id": "",
...
}
}
```
--------------------------------------------------------------------------------
PAGE: REST API
URL: https://docs.walt.id/community-stack/issuer/key-management/aws-kms/rest-api
RAW: https://docs.walt.id/community-stack/issuer/key-management/aws-kms/rest-api.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# REST API
The issuer API can leverage AWS key management service
to hook into the "cryptography as a service" or "encryption as a service" features provided
by AWS. With
that, the issuer API can sign and verify credentials whilst keeping the secrets (signing keys) in an external
secure environment.
If you are new to AWS kms make sure to check out the guides [here](https://docs.aws.amazon.com/kms/latest/developerguide/overview.html).
The rest of this document assumes you already know how AWS KMS works, and you have it setup.
## Method : Using AWS access keys and secret keys
### Prerequisites
- **AWS Account**: You need an active AWS account. If you don't have one, create it at [AWS Console](https://aws.amazon.com/console/).
### Create AWS Access Keys and Secret Keys
To securely interact with AWS services such as KMS, you need to configure your AWS access and secret keys. Here’s how to generate and configure these credentials:
1. Log in to your AWS Management Console.
2. Navigate to **IAM (Identity and Access Management)**.
3. In the left sidebar, click on **Users**, then choose your username.
4. Go to the **Security credentials** tab.
5. Under **Access keys**, click **Create access key**.
6. Copy both the **Access Key ID** and **Secret Access Key**. Store them securely.
### Key generation
To create the key you can use the onboard endpoint provided by the issuer API and provide the necessary parameters
to create the key in the AWS KMS service.
#### Creation via Issuer API
**Option: Request**
**Endpoint:**
`/onboard/issuer` | [API Reference](https://issuer.demo.walt.id/swagger/index.html#/Issuer%20onboarding/post_onboard_issuer)
**Example Request**
```bash
curl -X 'POST' \
'https://issuer.demo.walt.id/onboard/issuer' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"key": {
"backend": "aws-rest-api",
"keyType": "secp256r1",
"config": {
"auth": {
"accessKeyId": "AKIAW......U2TQU5F",
"secretAccessKey": "6YDrEN1t+......I3OCwSc7Sr",
"region": "eu-central-1"
},
"tags": {
"key": "value"
}
}
},
"did": {
"method": "jwk"
}
}'
```
**Body**
```json
{
"key": {
"backend": "aws-rest-api",
"keyType": "secp256r1",
"config": {
"auth": {
"accessKeyId": "AKIAW......U2TQU5F",
"secretAccessKey": "6YDrEN1t+......I3OCwSc7Sr",
"region": "eu-central-1"
},
"tags": {
"key": "value"
}
}
},
"did": {
"method": "jwk"
}
}
```
**Body Parameters**
- `key`
- `backend`: _String_ - Specifies the storage type of key. It can be `jwk` (managed by you), `aws-rest-api` (managed by
AWS KMS using Rest api requests ) and others. Learn more about different types [here](https://docs.walt.id/community-stack/issuer/key-management/overview.md).
- `keyType`: _String_ - the algorithm used to generate the key. Supported AWS KMS values are `secp256r1`, `secp384r1`, `secp521r1`, `secp256k1`, `RSA`, `RSA3072`, and `RSA4096`. `Ed25519` is not supported by AWS KMS signing keys.
- `config`
- `accessKeyId`: _String_ - The access key id for AWS KMS.
- `secretAccessKey`: _String_ - The secret access key for AWS KMS.
- `region`: _String_ - The region where the AWS KMS service is located.
- `tags`: _Object_ - The metadata tags to add to the key for [AWS](https://docs.aws.amazon.com/kms/latest/developerguide/tagging-keys.html)
- `did`:
- `method`: _String_ - Specifies the DID method. It can be key, jwk, web, cheqd.
**Option: Response**
**Example Response**
The onboard/issuer endpoint will return an object containing both the generated key in JWK format and the related DID.
```json
{
"issuerKey": {
"type": "aws-rest-api",
"config": {
"auth": {
"accessKeyId": "AKIAW......U2TQU5F",
"secretAccessKey": "6YDrEN1t+......I3OCwSc7Sr",
"region": "eu-central-1"
},
"tags": {
"key": "value"
}
},
"id": "324ebf67-6bcc-4439-8b81-260bf0a82532",
"_publicKey": "{\"kty\":\"EC\",\"crv\":\"P-256\",\"x\":\"RfvJGDXOjz3NOSKgZ0VlijXST8S-j96DrG0C1AMNAK8\",\"y\":\"vSt2q7yIy0-AhAirMuuDmUScxkgf4JVfId-vTETraQA\"}",
"_keyType": "secp256r1"
},
"issuerDid": "did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IlJmdkpHRFhPanozTk9TS2daMFZsaWpYU1Q4Uy1qOTZEckcwQzFBTU5BSzgiLCJ5IjoidlN0MnE3eUl5MC1BaEFpck11dURtVVNjeGtnZjRKVmZJZC12VEVUcmFRQSJ9"
}
```
**Properties**
- `type`: _String_ - the type of key can be either "aws" when using AWS KMS or "jwk" when providing the key in
full as JWK.
- `config`
- `accessKeyId`: _String_ - The access key id for AWS KMS.
- `secretAccessKey`: _String_ - The secret access key for AWS KMS.
- `region`: _String_ - The region where the AWS KMS service is located.
- `id`: _String_ - the ID of the key in the Transit Engine.
- `_publicKey` (optional): _Array_ - The public key can be fetched by the issuer API or directly provided, saving
resources and reducing network requests.
- `_keyType` (optional): _String_ - The key type can be fetched by the issuer API or directly provided, saving resources
and reducing network requests.
- `issuerDid`: _String_ - the DID of the issuer.
### Key Usage
Once you have successfully created a key that is one of the supported types listed above, you can use it in
sign and issue operations offered by the issuer API.
If you've already had a look at
our `/sign`, `/issue`, `/batchIssue`
endpoints, you have seen that they all follow a similar request body structure, where the key that should be
used for signing credentials is provided via the `issuerKey` property. Now instead of providing the key as JWK, we
provide
a reference to a key stored in Vault with the required parameters and access credentials.
Below you can see an example of
`issuerKey` object referencing a key stored in AWS.
**Option: Example IssuerKey Object**
```json
{
"issuerKey": {
"type": "aws-rest-api",
"config": {
"auth": {
"accessKeyId": "AKIAW......U2TQU5F",
"secretAccessKey": "6YDrEN1t+......I3OCwSc7Sr",
"region": "eu-central-1"
},
"tags": {
"key": "value"
}
},
"id": "324ebf67-6bcc-4439-8b81-260bf0a82532",
"_publicKey": "{\"kty\":\"EC\",\"crv\":\"P-256\",\"x\":\"RfvJGDXOjz3NOSKgZ0VlijXST8S-j96DrG0C1AMNAK8\",\"y\":\"vSt2q7yIy0-AhAirMuuDmUScxkgf4JVfId-vTETraQA\"}",
"_keyType": "secp256r1"
},
"issuerDid": "did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IlJmdkpHRFhPanozTk9TS2daMFZsaWpYU1Q4Uy1qOTZEckcwQzFBTU5BSzgiLCJ5IjoidlN0MnE3eUl5MC1BaEFpck11dURtVVNjeGtnZjRKVmZJZC12VEVUcmFRQSJ9"
}
```
**Properties**
- `type`: _String_ - the type of key can be either "aws" when using AWS KMS or "jwk" when providing the key in
full as JWK.
-
- `id`: _String_ - the ID of the key in the Transit Engine.
- `config`
- `accessKeyId`: _String_ - The access key id for AWS KMS.
- `secretAccessKey`: _String_ - The secret access key for AWS KMS.
- `region`: _String_ - The region where the AWS KMS service is located.
- `_publicKey` (optional): _Array_ - The public key can be fetched by the issuer API or directly provided, saving
resources and reducing network requests.
- `_keyType` (optional): _String_ - The key type can be fetched by the issuer API or directly provided, saving resources
and reducing network requests.
**Option: Example Issuance Request**
## Example Issuance Request
Below you can see example issuance request
to [`jwt/issue`](https://issuer.demo.walt.id/swagger/index.html#/Credential%20Issuance/post_openid4vc_jwt_issue) using
a key created in AWS kms to
sign the credential.
[Api Reference](https://issuer.demo.walt.id/swagger/index.html#/Credential%20Issuance/post_openid4vc_jwt_issue)
```bash
curl -X 'POST' \
'https://issuer.demo.walt.id/openid4vc/jwt/issue' \
-H 'accept: text/plain' \
-H 'statusCallbackUri: https://example.com/$id' \
-H 'Content-Type: application/json' \
-d '{
"issuerKey": {
"type": "aws-rest-api",
"config": {
"auth": {
"accessKeyId": "AKIAW......U2TQU5F",
"secretAccessKey": "6YDrEN1t+......I3OCwSc7Sr",
"region": "eu-central-1"
},
"tags": {
"key": "value"
}
},
"id": "324ebf67-6bcc-4439-8b81-260bf0a82532",
"_publicKey": "{\"kty\":\"EC\",\"crv\":\"P-256\",\"x\":\"RfvJGDXOjz3NOSKgZ0VlijXST8S-j96DrG0C1AMNAK8\",\"y\":\"vSt2q7yIy0-AhAirMuuDmUScxkgf4JVfId-vTETraQA\"}",
"_keyType": "secp256r1"
},
"issuerDid": "did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IlJmdkpHRFhPanozTk9TS2daMFZsaWpYU1Q4Uy1qOTZEckcwQzFBTU5BSzgiLCJ5IjoidlN0MnE3eUl5MC1BaEFpck11dURtVVNjeGtnZjRKVmZJZC12VEVUcmFRQSJ9",
"credentialData": {
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://purl.imsglobal.org/spec/ob/v3p0/context.json"
],
"id": "urn:uuid:THIS WILL BE REPLACED WITH DYNAMIC DATA FUNCTION (see below)",
"type": [
"VerifiableCredential",
"OpenBadgeCredential"
],
"name": "JFF x vc-edu PlugFest 3 Interoperability",
...
},
"mapping": {
"id": "",
...
}
}'
```
**Header Parameters**
- `statusCallbackUri`: _URL_ - Receive updates on the created issuance process, e.g. when a credential was successfully
claimed. The parameter expects a URL which can accept a JSON POST request. The URL can also hold a `$id`, which will
be
replaced by the issuance session id. For example: `https://myurl.com/$id`, `https://myurl.com`
or `https://myurl.com/test/$id`
Expand To Learn More
**Body**
The data send to the provided URL will contain a JSON body:
- `id` : _String_ - the issuance session id
- `type`: _String_ - the event type
- `data`: _JsonObject_ - the data for the event
**Event Types**
Possible events (event types) and their data are:
- `resolved_credential_offer` with the credential offer as JSON (in our Web Wallet: called when the issuance offer
is
entered into the wallet, but not processing / accepted yet)
- `requested_token` with the issuance request for the token as json object (called for the token required to receive
the
credentials)
Credential issuance (called for every credential that's issued (= requested from wallet))
- `jwt_issue` with `jwt` being the issued jwt
- `sdjwt_issue` with `sdjwt` being the issued sdjwt
- `batch_jwt_issue` with `jwt` being the issued jwt
- `batch_sdjwt_issue` with `sdjwt` being the issued sdjwt
- `generated_mdoc` with `mdoc` being the CBOR (HEX) of the signed mdoc
To allow for secure business logic flows, if a callback URL is set, and it cannot be reached, the issuance will not
commence further (after that point). If no callback URL is set, the issuance logic does not change in any way.
**Body**
As you can see for the property `issuerKey` we only provided the required parameters of the TSE Key Reference Object
described above and left out `_publicKey` and `_keyType`
```json
{
"issuerKey": {
"type": "aws-rest-api",
"config": {
"auth": {
"accessKeyId": "AKIAW......U2TQU5F",
"secretAccessKey": "6YDrEN1t+......I3OCwSc7Sr",
"region": "eu-central-1"
},
"tags": {
"key": "value"
}
},
"id": "324ebf67-6bcc-4439-8b81-260bf0a82532",
"_publicKey": "{\"kty\":\"EC\",\"crv\":\"P-256\",\"x\":\"RfvJGDXOjz3NOSKgZ0VlijXST8S-j96DrG0C1AMNAK8\",\"y\":\"vSt2q7yIy0-AhAirMuuDmUScxkgf4JVfId-vTETraQA\"}",
"_keyType": "secp256r1"
},
"issuerDid": "did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IlJmdkpHRFhPanozTk9TS2daMFZsaWpYU1Q4Uy1qOTZEckcwQzFBTU5BSzgiLCJ5IjoidlN0MnE3eUl5MC1BaEFpck11dURtVVNjeGtnZjRKVmZJZC12VEVUcmFRQSJ9",
"credentialData": {
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://purl.imsglobal.org/spec/ob/v3p0/context.json"
],
"id": "urn:uuid:THIS WILL BE REPLACED WITH DYNAMIC DATA FUNCTION (see below)",
"type": [
"VerifiableCredential",
"OpenBadgeCredential"
],
"name": "JFF x vc-edu PlugFest 3 Interoperability",
...
},
"mapping": {
"id": "",
...
}
}
```
## Method : Instance Authentication
### Prerequisites
- **AWS Account**: You need an active AWS account. If you don't have one, create it at [AWS Console](https://aws.amazon.com/console/).
### Set Up an IAM Role for the EC2 Instance
To authenticate your instance with AWS, You can Set Up an IAM Role for the EC2 Instance in AWS Console by following the steps below:
1. **Log in to your AWS Management Console**.
2. **Navigate to IAM (Identity and Access Management)**.
3. **In the left sidebar, click on Roles**.
4. **Click on Create Role**.
5. **Select EC2 as the service that will use this role**.
6. **Click on Next: Permissions**.
7. **Attach the necessary permissions to the role**.
8. **Click on Next: Tags**.
9. **Add tags if necessary**.
10. **Click on Next: Review**.
11. **Enter a name for the role**.
12. **Click on Create Role**.
13. **Attach the role to the EC2 instance**.
14. **Navigate to the EC2 instance in the AWS Management Console**.
15. **Click on Actions > Security > Modify IAM Role**.
16. **Select the role you created**.
17. **Click on Save**.
18. **The EC2 instance is now authenticated with AWS**.
19. **You can now use the AWS SDK to interact with AWS services**.

When you have set up the IAM role for the EC2 instance,
you can use it to authenticate with AWS services.
### Key generation
To create the key you can use the onboard endpoint provided by the issuer API and provide the necessary parameters
to create the key in the AWS KMS service.
#### Creation via Issuer API
**Option: Request**
**Endpoint:**
`/onboard/issuer` | [API Reference](https://issuer.demo.walt.id/swagger/index.html#/Issuer%20onboarding/post_onboard_issuer)
**Example Request**
```bash
curl -X 'POST' \
'https://issuer.demo.walt.id/onboard/issuer' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"key": {
"backend": "aws-rest-api",
"keyType": "secp256r1",
"config": {
"auth": {
"roleName": "AccessRole",
"region": "eu-central-1"
},
"tags": {
"key": "value"
}
}
},
"did": {
"method": "jwk"
}
}'
```
**Body**
```json
{
"key": {
"backend": "aws-rest-api",
"keyType": "secp256r1",
"config": {
"auth": {
"roleName": "AccessRole",
"region": "eu-central-1"
},
"tags": {
"key": "value"
}
}
},
"did": {
"method": "jwk"
}
}
```
**Body Parameters**
- `key`
- `backend`: _String_ - Specifies the storage type of key. It can be `jwk` (managed by you), `aws-rest-api` (managed by
AWS KMS using Rest api requests ) and others. Learn more about different types [here](https://docs.walt.id/community-stack/issuer/key-management/overview.md).
- `keyType`: _String_ - the algorithm used to generate the key. Supported AWS KMS values are `secp256r1`, `secp384r1`, `secp521r1`, `secp256k1`, `RSA`, `RSA3072`, and `RSA4096`. `Ed25519` is not supported by AWS KMS signing keys.
- `config`
- `roleName`: _String_ - The role name for AWS KMS.
- `region`: _String_ - The region where the AWS KMS service is located.
- `tags`: _Object_ - The metadata tags to add to the key for [AWS](https://docs.aws.amazon.com/kms/latest/developerguide/tagging-keys.html)
- `did`:
- `method`: _String_ - Specifies the DID method. It can be key, jwk, web, cheqd.
**Option: Response**
**Example Response**
The onboard/issuer endpoint will return an object containing both the generated key in JWK format and the related DID.
```json
{
"issuerKey": {
"type": "aws-rest-api",
"config": {
"auth": {
"roleName": "AccessRole",
"region": "eu-central-1"
},
"tags": {
"key": "value"
}
},
"id": "324ebf67-6bcc-4439-8b81-260bf0a82532",
"_publicKey": "{\"kty\":\"EC\",\"crv\":\"P-256\",\"x\":\"RfvJGDXOjz3NOSKgZ0VlijXST8S-j96DrG0C1AMNAK8\",\"y\":\"vSt2q7yIy0-AhAirMuuDmUScxkgf4JVfId-vTETraQA\"}",
"_keyType": "secp256r1"
},
"issuerDid": "did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IlJmdkpHRFhPanozTk9TS2daMFZsaWpYU1Q4Uy1qOTZEckcwQzFBTU5BSzgiLCJ5IjoidlN0MnE3eUl5MC1BaEFpck11dURtVVNjeGtnZjRKVmZJZC12VEVUcmFRQSJ9"
}
```
**Properties**
- `type`: _String_ - the type of key can be either "aws" when using AWS KMS or "jwk" when providing the key in
full as JWK.
- `config`
- `roleName`: _String_ - The role name for AWS KMS.
- `region`: _String_ - The region where the AWS KMS service is located.
- `tags`: _Object_ - The metadata tags to add to the key for [AWS](https://docs.aws.amazon.com/kms/latest/developerguide/tagging-keys.html)
- `id`: _String_ - the ID of the key in the Transit Engine.
- `_publicKey` (optional): _Array_ - The public key can be fetched by the issuer API or directly provided, saving
resources and reducing network requests.
- `_keyType` (optional): _String_ - The key type can be fetched by the issuer API or directly provided, saving resources
and reducing network requests.
- `issuerDid`: _String_ - the DID of the issuer.
### Key Usage
Once you have successfully created a key that is one of the supported types listed above, you can use it in
sign and issue operations offered by the issuer API.
If you've already had a look at
our `/sign`, `/issue`, `/batchIssue`
endpoints, you have seen that they all follow a similar request body structure, where the key that should be
used for signing credentials is provided via the `issuerKey` property. Now instead of providing the key as JWK, we
provide
a reference to a key stored in Vault with the required parameters and access credentials.
Below you can see an example of
`issuerKey` object referencing a key stored in AWS.
**Option: Example IssuerKey Object**
```json
{
"issuerKey": {
"type": "aws-rest-api",
"config": {
"auth": {
"roleName": "AccessRole",
"region": "eu-central-1"
},
"tags": {
"key": "value"
}
},
"id": "324ebf67-6bcc-4439-8b81-260bf0a82532",
"_publicKey": "{\"kty\":\"EC\",\"crv\":\"P-256\",\"x\":\"RfvJGDXOjz3NOSKgZ0VlijXST8S-j96DrG0C1AMNAK8\",\"y\":\"vSt2q7yIy0-AhAirMuuDmUScxkgf4JVfId-vTETraQA\"}",
"_keyType": "secp256r1"
},
"issuerDid": "did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IlJmdkpHRFhPanozTk9TS2daMFZsaWpYU1Q4Uy1qOTZEckcwQzFBTU5BSzgiLCJ5IjoidlN0MnE3eUl5MC1BaEFpck11dURtVVNjeGtnZjRKVmZJZC12VEVUcmFRQSJ9"
}
```
**Properties**
- `type`: _String_ - the type of key can be either "aws" when using AWS KMS or "jwk" when providing the key in
full as JWK.
-
- `id`: _String_ - the ID of the key in the Transit Engine.
- `config`
- `accessKeyId`: _String_ - The access key id for AWS KMS.
- `secretAccessKey`: _String_ - The secret access key for AWS KMS.
- `region`: _String_ - The region where the AWS KMS service is located.
- `_publicKey` (optional): _Array_ - The public key can be fetched by the issuer API or directly provided, saving
resources and reducing network requests.
- `_keyType` (optional): _String_ - The key type can be fetched by the issuer API or directly provided, saving resources
and reducing network requests.
**Option: Example Issuance Request**
## Example Issuance Request
Below you can see example issuance request
to [`jwt/issue`](https://issuer.demo.walt.id/swagger/index.html#/Credential%20Issuance/post_openid4vc_jwt_issue) using
a key created in AWS kms to
sign the credential.
[Api Reference](https://issuer.demo.walt.id/swagger/index.html#/Credential%20Issuance/post_openid4vc_jwt_issue)
```bash
curl -X 'POST' \
'https://issuer.demo.walt.id/openid4vc/jwt/issue' \
-H 'accept: text/plain' \
-H 'statusCallbackUri: https://example.com/$id' \
-H 'Content-Type: application/json' \
-d '{
"issuerKey": {
"type": "aws-rest-api",
"config": {
"auth": {
"roleName": "AccessRole",
"region": "eu-central-1"
},
"tags": {
"key": "value"
}
},
"id": "324ebf67-6bcc-4439-8b81-260bf0a82532",
"_publicKey": "{\"kty\":\"EC\",\"crv\":\"P-256\",\"x\":\"RfvJGDXOjz3NOSKgZ0VlijXST8S-j96DrG0C1AMNAK8\",\"y\":\"vSt2q7yIy0-AhAirMuuDmUScxkgf4JVfId-vTETraQA\"}",
"_keyType": "secp256r1"
},
"issuerDid": "did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IlJmdkpHRFhPanozTk9TS2daMFZsaWpYU1Q4Uy1qOTZEckcwQzFBTU5BSzgiLCJ5IjoidlN0MnE3eUl5MC1BaEFpck11dURtVVNjeGtnZjRKVmZJZC12VEVUcmFRQSJ9",
"credentialData": {
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://purl.imsglobal.org/spec/ob/v3p0/context.json"
],
"id": "urn:uuid:THIS WILL BE REPLACED WITH DYNAMIC DATA FUNCTION (see below)",
"type": [
"VerifiableCredential",
"OpenBadgeCredential"
],
"name": "JFF x vc-edu PlugFest 3 Interoperability",
...
},
"mapping": {
"id": "",
...
}
}'
```
**Header Parameters**
- `statusCallbackUri`: _URL_ - Receive updates on the created issuance process, e.g. when a credential was successfully
claimed. The parameter expects a URL which can accept a JSON POST request. The URL can also hold a `$id`, which will
be
replaced by the issuance session id. For example: `https://myurl.com/$id`, `https://myurl.com`
or `https://myurl.com/test/$id`
Expand To Learn More
**Body**
The data send to the provided URL will contain a JSON body:
- `id` : _String_ - the issuance session id
- `type`: _String_ - the event type
- `data`: _JsonObject_ - the data for the event
**Event Types**
Possible events (event types) and their data are:
- `resolved_credential_offer` with the credential offer as JSON (in our Web Wallet: called when the issuance offer
is
entered into the wallet, but not processing / accepted yet)
- `requested_token` with the issuance request for the token as json object (called for the token required to receive
the
credentials)
Credential issuance (called for every credential that's issued (= requested from wallet))
- `jwt_issue` with `jwt` being the issued jwt
- `sdjwt_issue` with `sdjwt` being the issued sdjwt
- `batch_jwt_issue` with `jwt` being the issued jwt
- `batch_sdjwt_issue` with `sdjwt` being the issued sdjwt
- `generated_mdoc` with `mdoc` being the CBOR (HEX) of the signed mdoc
To allow for secure business logic flows, if a callback URL is set, and it cannot be reached, the issuance will not
commence further (after that point). If no callback URL is set, the issuance logic does not change in any way.
**Body**
As you can see for the property `issuerKey` we only provided the required parameters of the TSE Key Reference Object
described above and left out `_publicKey` and `_keyType`
```json
{
"issuerKey": {
"type": "aws-rest-api",
"config": {
"auth": {
"roleName": "AccessRole",
"region": "eu-central-1"
},
"tags": {
"key": "value"
}
},
"id": "324ebf67-6bcc-4439-8b81-260bf0a82532",
"_publicKey": "{\"kty\":\"EC\",\"crv\":\"P-256\",\"x\":\"RfvJGDXOjz3NOSKgZ0VlijXST8S-j96DrG0C1AMNAK8\",\"y\":\"vSt2q7yIy0-AhAirMuuDmUScxkgf4JVfId-vTETraQA\"}",
"_keyType": "secp256r1"
},
"issuerDid": "did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IlJmdkpHRFhPanozTk9TS2daMFZsaWpYU1Q4Uy1qOTZEckcwQzFBTU5BSzgiLCJ5IjoidlN0MnE3eUl5MC1BaEFpck11dURtVVNjeGtnZjRKVmZJZC12VEVUcmFRQSJ9",
"credentialData": {
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://purl.imsglobal.org/spec/ob/v3p0/context.json"
],
"id": "urn:uuid:THIS WILL BE REPLACED WITH DYNAMIC DATA FUNCTION (see below)",
"type": [
"VerifiableCredential",
"OpenBadgeCredential"
],
"name": "JFF x vc-edu PlugFest 3 Interoperability",
...
},
"mapping": {
"id": "",
...
}
}
```
--------------------------------------------------------------------------------
PAGE: Azure Key Vault
URL: https://docs.walt.id/community-stack/issuer/key-management/azure-kms/overview
RAW: https://docs.walt.id/community-stack/issuer/key-management/azure-kms/overview.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Overview
The issuer API supports Azure Key Vault as a key management solution. This allows you to store your cryptographic keys
in a secure and scalable way. The API is designed to be stateless and does not store cryptographic key material.
Therefore, every API call requires the cryptographic key to be provided. When signing and issuing a credential
e.g. via [**OID4VCI**](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md), either the complete key in JWK format or a reference to a key stored in Azure Key Vault is needed.
In production systems, the recommended approach is to use Azure Key Vault so that the private key used to sign a
credential never leaves a secure environment.
We support the following key types offered by Azure Key Vault:
- RSA
- SECP256R1
- SECP256K1
The platform offers two integration options for Azure Key Vault:
- A native Azure SDK–based backend registered as `azure`
- A REST-based backend registered as `azure-rest-api`
Both backends allow you to generate keys, sign credentials using keys stored in Azure, and retrieve public keys,
while keeping private key material inside Azure Key Vault.
## Key Features
- Native Azure SDK integration for optimal performance (`azure` backend)
- REST-based integration for environments where direct SDK usage is not possible (`azure-rest-api` backend)
- Seamless key management through Azure Key Vault
- Support for attaching metadata tags to keys managed via the issuer API
## Authentication
Depending on which backend you use, authentication towards Azure Key Vault can rely on:
- Client credentials (client ID, client secret, tenant ID)
- Managed identities (for SDK-based integrations, recommended for production)
In all cases, the issuer API only stores references and configuration required to access keys; the keys themselves
are created and held by Azure Key Vault.
## Get Started
- [Azure Key Vault SDK](https://docs.walt.id/community-stack/issuer/key-management/azure-kms/sdk.md) - Use keys from Azure Key Vault to sign credentials.
- [Azure Key Vault REST API](https://docs.walt.id/community-stack/issuer/key-management/azure-kms/rest-api.md) - Use keys from Azure Key Vault via REST API.
--------------------------------------------------------------------------------
PAGE: Azure Key Vault (Azure SDK)
URL: https://docs.walt.id/community-stack/issuer/key-management/azure-kms/sdk
RAW: https://docs.walt.id/community-stack/issuer/key-management/azure-kms/sdk.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Azure Key Vault (Azure SDK)
Azure Key Vault is a managed cloud service for securely storing and using cryptographic keys, secrets, and certificates.
This document describes the **native Azure SDK–based integration** in the walt.id platform, which enables key
generation, signing, verification, and deletion directly via the Azure SDK instead of raw REST calls.
This backend is registered as **`azure`** in the key manager.
The legacy REST-based implementation is now explicitly named **`azure-rest-api`**.
---
## Prerequisites
- An Azure account with an active subscription
https://azure.microsoft.com/free/
- An Azure Key Vault instance
https://learn.microsoft.com/azure/key-vault/general/quick-create-portal
- An Azure AD application (service principal)
- One of the following authentication methods:
- Client Secret
- Managed Identity (recommended for production)
---
## Supported Features
- Key generation in Azure Key Vault
- Signing using Azure-managed private keys
- Public key retrieval
- Key deletion
---
## Key Generation
Key creation is done using the standard onboarding endpoint.
The only difference is the backend type and configuration structure.
### Creation via Issuer API (Azure SDK)
**Option: Request**
**Endpoint**
`/onboard/issuer`
[API Reference](https://issuer.demo.walt.id/swagger/index.html#/Issuer%20onboarding/post_onboard_issuer)
**Example Request**
```bash
curl -X POST \
'https://issuer.demo.walt.id/onboard/issuer' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"key": {
"backend": "azure",
"keyType": "secp256r1",
"config": {
"auth": {
"keyVaultUrl": "url to the vault"
},
"tags": {
"key": "value"
}
}
},
"did": {
"method": "jwk"
}
}'
```
**Body Parameters**
- `key`
- `backend`: _String_ - Must be `azure` for Azure SDK integration
- `keyType`: _String_ - Supported: `secp256r1`, `ed25519` (depending on Key Vault capabilities)
- `config`
- `keyVaultUrl`: _String_ - The URL of your Azure Key Vault
- `tags`: _Object_ - The metadata tags to add to the key for [Azure](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/tag-resources)
- `did`
- `method`: _String_ - Supported: `key`, `jwk`, `web`, `cheqd`
**Option: Response**
```json
{
"issuerKey": {
"type": "azure",
"id": "https://.vault.azure.net/keys/issuer-key/1234567890abcdef",
"_keyType": "secp256r1",
"_publicKey": {
"type": "jwk",
"jwk": {
"kty": "EC",
"crv": "P-256",
"kid": "https://.vault.azure.net/keys/issuer-key/1234567890abcdef",
"x": "",
"y": "",
"key_ops": ["sign", "verify"]
}
}
},
"issuerDid": "did:jwk:..."
}
```
**Properties**
- `type`: _String_ - Always `azure` for Azure SDK keys
- `id`: _String_ - Full Azure Key Vault key identifier
- `_keyType` (optional): _String_
- `_publicKey` (optional): _Object_
- `issuerDid`: _String_
# Key Usage
Once created, Azure SDK–backed keys are used exactly like any other key backend.
The issuer API will transparently delegate cryptographic operations to Azure Key Vault.
The key reference is passed via the issuerKey object.
Example IssuerKey Object (Azure SDK) :
```json
{
"issuerKey": {
"type": "azure",
"id": "https://.vault.azure.net/keys/issuer-key/1234567890abcdef"
},
"issuerDid": "did:jwk:..."
}
```
--------------------------------------------------------------------------------
PAGE: Azure Key Vault ( Rest API)
URL: https://docs.walt.id/community-stack/issuer/key-management/azure-kms/rest-api
RAW: https://docs.walt.id/community-stack/issuer/key-management/azure-kms/rest-api.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Azure Key Vault ( Rest API)
Azure Key Vault is a cloud service for securely storing and managing cryptographic keys, secrets, and certificates. It
provides a secure and reliable way to protect sensitive information and control access to keys used by cloud
applications and services.
## Prerequisites
- An Azure account with an active subscription. [Create an account](https://azure.microsoft.com/en-us/free/).
- An Azure Key Vault
instance. [Create a Key Vault](https://docs.microsoft.com/en-us/azure/key-vault/quick-create-portal).
- An Azure AD application with access to the Key
Vault. [Create an Azure AD application](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app).
## Setup
1. **Create an Azure AD application** with access to the Key Vault. Note down the `Application (client) ID` and
`Directory (tenant) ID`.
2. **Create a client secret** for the Azure AD application. Note down the `Client Secret`.
3. **Assign permissions** to the Azure AD application to access the Key Vault. Grant the `Key permissions` and
`Secret permissions` to the application.
4. **Get the Key Vault URL**. Note down the `Key Vault URL` from the Azure portal.
5. **Configure the issuer API** to use the Azure Key Vault. Provide the `Key Vault URL`, `Application ID`, `Tenant ID`,
and `Client Secret` in the issuer API configuration.
6. **Generate a key** in the Key Vault. Use the issuer API to generate a key in the Azure Key Vault.
7. **Sign and issue credentials** using the key stored in the Azure Key Vault.
## Key Generation
To create the key you can use the onboard endpoint provided by the issuer API and provide the necessary parameters
to create the key in the Azure Key Vault service.
#### Creation via Issuer API
**Option: Request**
**Endpoint:**
`/onboard/issuer` | [API Reference](https://issuer.demo.walt.id/swagger/index.html#/Issuer%20onboarding/post_onboard_issuer)
**Example Request**
```bash
curl -X 'POST' \
'https://issuer.demo.walt.id/onboard/issuer' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"key": {
"backend": "azure-rest-api",
"keyType": "secp256r1",
"config": {
"auth": {
"keyVaultUrl": "https://.vault.azure.net/",
"clientId": "",
"tenantId": "",
"clientSecret": ""
},
"tags": {
"key": "value"
}
}
},
"did": {
"method": "jwk"
}
}'
```
**Body**
```json
{
"key": {
"backend": "azure-rest-api",
"keyType": "secp256r1",
"config": {
"auth": {
"keyVaultUrl": "https://.vault.azure.net/",
"clientId": "",
"tenantId": "",
"clientSecret": ""
},
"tags": {
"key": "value"
}
}
},
"did": {
"method": "jwk"
}
}
```
**Body Parameters**
- `key`
- `backend`: _String_ - Specifies the storage type of key. It can be `jwk` (managed by you), `azure` (managed
by
Azure Key Vault using Rest api requests ) and others. Learn more about different
types [here](https://docs.walt.id/community-stack/issuer/key-management/overview.md).
- `keyType`: _String_ - the algorithm used to generate the key. Supported Azure Key Vault values are `secp256r1`, `secp384r1`, `secp521r1`, `secp256k1`, `RSA`, `RSA3072`, and `RSA4096`. `Ed25519` is not supported.
- `config`
- `keyVaultUrl`: _String_ - The URL of the Azure Key Vault.
- `clientId`: _String_ - The client ID of the Azure AD application.
- `tenantId`: _String_ - The tenant ID of the Azure AD application.
- `clientSecret`: _String_ - The client secret of the Azure AD application.
- `tags`: _Object_ - The metadata tags to add to the key for [Azure](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/tag-resources)
- `did`:
- `method`: _String_ - Specifies the DID method. It can be key, jwk, web, cheqd.
**Option: Response**
**Example Response**
The onboard/issuer endpoint will return an object containing both the generated key in JWK format and the related DID.
```json
{
"issuerKey": {
"type": "azure-rest-api",
"id": "https://kv-nhd-vp-core-dev.vault.azure.net/keys/541693094/be4a95477f274b38826938a92f990fd5",
"auth": {
"keyVaultUrl": "https://.vault.azure.net/",
"clientId": "",
"tenantId": "",
"clientSecret": ""
},
"_keyType": "secp256r1",
"_publicKey": {
"type": "jwk",
"jwk": {
"kty": "EC",
"crv": "P-256",
"kid": "https://kv-nhd-vp-core-dev.vault.azure.net/keys/541693094/be4a95477f274b38826938a92f990fd5",
"key_ops": [
"sign",
"verify"
],
"x": "gWLUzx_B-36rcSdwlf78IjCRqVzchZFj6ino_ueu1y8",
"y": "dL6LiFRFDwSyQphljk5YB0gXGlB5eg8sPZV_m1DTl2o"
}
}
},
"issuerDid": "did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2Iiwia2lkIjoiaHR0cHM6Ly9rdi1uaGQtdnAtY29yZS1kZXYudmF1bHQuYXp1cmUubmV0L2tleXMvNTQxNjkzMDk0L2JlNGE5NTQ3N2YyNzRiMzg4MjY5MzhhOTJmOTkwZmQ1Iiwia2V5X29wcyI6WyJzaWduIiwidmVyaWZ5Il0sIngiOiJnV0xVenhfQi0zNnJjU2R3bGY3OElqQ1JxVnpjaFpGajZpbm9fdWV1MXk4IiwieSI6ImRMNkxpRlJGRHdTeVFwaGxqazVZQjBnWEdsQjVlZzhzUFpWX20xRFRsMm8ifQ"
}
```
**Properties**
- `type`: _String_ - the type of key can be either "aws" when using AWS KMS or "jwk" when providing the key in
full as JWK.
- `config`
- `keyVaultUrl`: _String_ - The URL of the Azure Key Vault.
- `clientId`: _String_ - The client ID of the Azure AD application.
- `tenantId`: _String_ - The tenant ID of the Azure AD application.
- `clientSecret`: _String_ - The client secret of the Azure AD application.
- `_publicKey` (optional): _Array_ - The public key can be fetched by the issuer API or directly provided, saving
resources and reducing network requests.
- `_keyType` (optional): _String_ - The key type can be fetched by the issuer API or directly provided, saving resources
and reducing network requests.
- `issuerDid`: _String_ - the DID of the issuer.
### Key Usage
Once you have successfully created a key that is one of the supported types listed above, you can use it in
sign and issue operations offered by the issuer API.
If you've already had a look at
our `/sign`, `/issue`, `/batchIssue`
endpoints, you have seen that they all follow a similar request body structure, where the key that should be
used for signing credentials is provided via the `issuerKey` property. Now instead of providing the key as JWK, we
provide
a reference to a key stored in Vault with the required parameters and access credentials.
Below you can see an example of
`issuerKey` object referencing a key stored in AWS.
**Option: Example IssuerKey Object**
```json
{
"issuerKey": {
"type": "azure-rest-api",
"id": "https://kv-nhd-vp-core-dev.vault.azure.net/keys/541693094/be4a95477f274b38826938a92f990fd5",
"auth": {
"keyVaultUrl": "https://.vault.azure.net/",
"clientId": "",
"tenantId": "",
"clientSecret": ""
},
"_keyType": "secp256r1",
"_publicKey": {
"type": "jwk",
"jwk": {
"kty": "EC",
"crv": "P-256",
"kid": "https://kv-nhd-vp-core-dev.vault.azure.net/keys/541693094/be4a95477f274b38826938a92f990fd5",
"key_ops": [
"sign",
"verify"
],
"x": "gWLUzx_B-36rcSdwlf78IjCRqVzchZFj6ino_ueu1y8",
"y": "dL6LiFRFDwSyQphljk5YB0gXGlB5eg8sPZV_m1DTl2o"
}
}
},
"issuerDid": "did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2Iiwia2lkIjoiaHR0cHM6Ly9rdi1uaGQtdnAtY29yZS1kZXYudmF1bHQuYXp1cmUubmV0L2tleXMvNTQxNjkzMDk0L2JlNGE5NTQ3N2YyNzRiMzg4MjY5MzhhOTJmOTkwZmQ1Iiwia2V5X29wcyI6WyJzaWduIiwidmVyaWZ5Il0sIngiOiJnV0xVenhfQi0zNnJjU2R3bGY3OElqQ1JxVnpjaFpGajZpbm9fdWV1MXk4IiwieSI6ImRMNkxpRlJGRHdTeVFwaGxqazVZQjBnWEdsQjVlZzhzUFpWX20xRFRsMm8ifQ"
}
```
**Properties**
- `type`: _String_ - the type of key can be either "aws" when using AWS KMS or "jwk" when providing the key in
full as JWK.
-
- `id`: _String_ - the ID of the key in the Transit Engine.
- `config`
- `keyVaultUrl`: _String_ - The URL of the Azure Key Vault.
- `clientId`: _String_ - The client ID of the Azure AD application.
- `tenantId`: _String_ - The tenant ID of the Azure AD application.
- `clientSecret`: _String_ - The client secret of the Azure AD application.
- `_publicKey` (optional): _Array_ - The public key can be fetched by the issuer API or directly provided, saving
resources and reducing network requests.
- `_keyType` (optional): _String_ - The key type can be fetched by the issuer API or directly provided, saving resources
and reducing network requests.
**Option: Example Issuance Request**
## Example Issuance Request
Below you can see example issuance request
to [`jwt/issue`](https://issuer.demo.walt.id/swagger/index.html#/Credential%20Issuance/post_openid4vc_jwt_issue) using
a key created in AWS kms to
sign the credential.
[Api Reference](https://issuer.demo.walt.id/swagger/index.html#/Credential%20Issuance/post_openid4vc_jwt_issue)
```bash
curl -X 'POST' \
'https://issuer.demo.walt.id/openid4vc/jwt/issue' \
-H 'accept: text/plain' \
-H 'statusCallbackUri: https://example.com/$id' \
-H 'Content-Type: application/json' \
-d '{
"issuerKey": {
"type": "azure-rest-api",
"id": "https://kv-nhd-vp-core-dev.vault.azure.net/keys/541693094/be4a95477f274b38826938a92f990fd5",
"auth": {
"keyVaultUrl": "https://.vault.azure.net/",
"clientId": "",
"tenantId": "",
"clientSecret": ""
},
"_keyType": "secp256r1",
"_publicKey": {
"type": "jwk",
"jwk": {
"kty": "EC",
"crv": "P-256",
"kid": "https://kv-nhd-vp-core-dev.vault.azure.net/keys/541693094/be4a95477f274b38826938a92f990fd5",
"key_ops": [
"sign",
"verify"
],
"x": "gWLUzx_B-36rcSdwlf78IjCRqVzchZFj6ino_ueu1y8",
"y": "dL6LiFRFDwSyQphljk5YB0gXGlB5eg8sPZV_m1DTl2o"
}
}
},
"issuerDid": "did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2Iiwia2lkIjoiaHR0cHM6Ly9rdi1uaGQtdnAtY29yZS1kZXYudmF1bHQuYXp1cmUubmV0L2tleXMvNTQxNjkzMDk0L2JlNGE5NTQ3N2YyNzRiMzg4MjY5MzhhOTJmOTkwZmQ1Iiwia2V5X29wcyI6WyJzaWduIiwidmVyaWZ5Il0sIngiOiJnV0xVenhfQi0zNnJjU2R3bGY3OElqQ1JxVnpjaFpGajZpbm9fdWV1MXk4IiwieSI6ImRMNkxpRlJGRHdTeVFwaGxqazVZQjBnWEdsQjVlZzhzUFpWX20xRFRsMm8ifQ"
"credentialData": {
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://purl.imsglobal.org/spec/ob/v3p0/context.json"
],
"id": "urn:uuid:THIS WILL BE REPLACED WITH DYNAMIC DATA FUNCTION (see below)",
"type": [
"VerifiableCredential",
"OpenBadgeCredential"
],
"name": "JFF x vc-edu PlugFest 3 Interoperability",
...
},
"mapping": {
"id": "",
...
}
}'
```
**Header Parameters**
- `statusCallbackUri`: _URL_ - Receive updates on the created issuance process, e.g. when a credential was successfully
claimed. The parameter expects a URL which can accept a JSON POST request. The URL can also hold a `$id`, which will
be
replaced by the issuance session id. For example: `https://myurl.com/$id`, `https://myurl.com`
or `https://myurl.com/test/$id`
Expand To Learn More
**Body**
The data send to the provided URL will contain a JSON body:
- `id` : _String_ - the issuance session id
- `type`: _String_ - the event type
- `data`: _JsonObject_ - the data for the event
**Event Types**
Possible events (event types) and their data are:
- `resolved_credential_offer` with the credential offer as JSON (in our Web Wallet: called when the issuance offer
is
entered into the wallet, but not processing / accepted yet)
- `requested_token` with the issuance request for the token as json object (called for the token required to receive
the
credentials)
Credential issuance (called for every credential that's issued (= requested from wallet))
- `jwt_issue` with `jwt` being the issued jwt
- `sdjwt_issue` with `sdjwt` being the issued sdjwt
- `batch_jwt_issue` with `jwt` being the issued jwt
- `batch_sdjwt_issue` with `sdjwt` being the issued sdjwt
- `generated_mdoc` with `mdoc` being the CBOR (HEX) of the signed mdoc
To allow for secure business logic flows, if a callback URL is set, and it cannot be reached, the issuance will not
commence further (after that point). If no callback URL is set, the issuance logic does not change in any way.
**Body**
As you can see for the property `issuerKey` we only provided the required parameters of the TSE Key Reference Object
described above and left out `_publicKey` and `_keyType`
```json
{
"issuerKey": {
"type": "azure-rest-api",
"id": "https://kv-nhd-vp-core-dev.vault.azure.net/keys/541693094/be4a95477f274b38826938a92f990fd5",
"auth": {
"keyVaultUrl": "https://.vault.azure.net/",
"clientId": "",
"tenantId": "",
"clientSecret": ""
},
"_keyType": "secp256r1",
"_publicKey": {
"type": "jwk",
"jwk": {
"kty": "EC",
"crv": "P-256",
"kid": "https://kv-nhd-vp-core-dev.vault.azure.net/keys/541693094/be4a95477f274b38826938a92f990fd5",
"key_ops": [
"sign",
"verify"
],
"x": "gWLUzx_B-36rcSdwlf78IjCRqVzchZFj6ino_ueu1y8",
"y": "dL6LiFRFDwSyQphljk5YB0gXGlB5eg8sPZV_m1DTl2o"
}
}
},
"issuerDid": "did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2Iiwia2lkIjoiaHR0cHM6Ly9rdi1uaGQtdnAtY29yZS1kZXYudmF1bHQuYXp1cmUubmV0L2tleXMvNTQxNjkzMDk0L2JlNGE5NTQ3N2YyNzRiMzg4MjY5MzhhOTJmOTkwZmQ1Iiwia2V5X29wcyI6WyJzaWduIiwidmVyaWZ5Il0sIngiOiJnV0xVenhfQi0zNnJjU2R3bGY3OElqQ1JxVnpjaFpGajZpbm9fdWV1MXk4IiwieSI6ImRMNkxpRlJGRHdTeVFwaGxqazVZQjBnWEdsQjVlZzhzUFpWX20xRFRsMm8ifQ"
"credentialData": {
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://purl.imsglobal.org/spec/ob/v3p0/context.json"
],
"id": "urn:uuid:THIS WILL BE REPLACED WITH DYNAMIC DATA FUNCTION (see below)",
"type": [
"VerifiableCredential",
"OpenBadgeCredential"
],
"name": "JFF x vc-edu PlugFest 3 Interoperability",
...
},
"mapping": {
"id": "",
...
}
}
```
--------------------------------------------------------------------------------
PAGE: Overview
URL: https://docs.walt.id/community-stack/issuer/credential-issuance/overview
RAW: https://docs.walt.id/community-stack/issuer/credential-issuance/overview.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Overview
## How to create & issue different types of credentials
- [W3C VCs](https://docs.walt.id/community-stack/issuer/credential-issuance/vc-oid4vc.md) - issue W3C Verifiable Credentials with JWT or
SD-JWT signatures.
- [SD-JWT VCs](https://docs.walt.id/community-stack/issuer/credential-issuance/sd-jwt-vc-oid4vc.md) - issue IETF SD-JWT VC credentials.
- [mDLs](https://docs.walt.id/community-stack/issuer/credential-issuance/mdl-oid4vc.md) - issue Mobile Driver’s License (ISO/IEC 18013-5
mDLs) credentials.
## Populate Credential Data At The Time Of Issuance
- [Data Functions](https://docs.walt.id/community-stack/issuer/credential-issuance/data-functions.md) - ensure issued credentials always
contain the most current and accurate information.
--------------------------------------------------------------------------------
PAGE: How to Issue W3C Verifiable Credentials (JWT / SD-JWT) via OID4VCI with walt.id
URL: https://docs.walt.id/community-stack/issuer/credential-issuance/vc-oid4vc
RAW: https://docs.walt.id/community-stack/issuer/credential-issuance/vc-oid4vc.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# How to Issue W3C Verifiable Credentials (JWT / SD-JWT) via OID4VCI with walt.id
**TL;DR**
**What you’ll learn:**
- Generate an issuer signing key and DID with the `/onboard/issuer` endpoint.
- Prepare credential data for a W3C Verifiable Credential template (e.g. a UniversityDegree).
- Call the OID4VCI issuance endpoints for JWT and SD-JWT credentials with the required headers and body parameters.
- Create credential offer URLs that any compliant wallet can redeem for a credential.
**Relevant concepts (optional):**
- [Verifiable Credentials (W3C)](https://docs.walt.id/concepts/digital-credentials/verifiable-credentials-w3c.md) – W3C standard for digital credentials.
- [OID4VCI](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md) – protocol for issuing verifiable credentials to wallets.
- [SD-JWT VCs](https://docs.walt.id/concepts/digital-credentials/sd-jwt-vc.md) – selective-disclosure credential format based on JSON Web Tokens.
- [DIDs and DID methods](https://docs.walt.id/concepts/decentralised-identifiers.md) – identifiers used to represent issuers and holders.
Video: https://youtu.be/VZrYZrQPbw0?t=147
This guide provides a comprehensive walkthrough for issuing a Verifiable Credential based on
the [W3C standard](https://www.w3.org/TR/vc-data-model-2.0/)
with a JWT or SD-JWT signature using the walt.id issuer API.
The issuance process will utilize the
[OID4VCI protocol](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-ID1.html).
**Verifiable Credential (VC)**: A digital equivalent of physical credentials such as a driver's license or university
degree.
VCs are cryptographically secure, privacy-respecting, and instantly verifiable.
**OID4VCI**: A protocol specifying how parties can issue credentials and present them in a way that's consistent
and secure across platforms ensuring interoperability.
## Setup
See how to access to the issuer API below.
- [Deployed (Testing Only)](https://issuer.demo.walt.id/swagger/index.html) - Use our deployed version for testing.
- [Local](https://docs.walt.id/community-stack/issuer/setup.md) - Run the API in your environment with our open-source setup.
## Preparing for Issuance: Key Components
Before issuing a verifiable credential, you'll need:
1. **Raw Credential Data**: The information you want to issue as a VC, typically following a specific template or
schema.
2. **Signing Key**: A cryptographic key used to sign the credential, confirming its authenticity and integrity during
verification.
If you don't make any specific configurations, the walt.id issuer API supports:
- **Default credential types** – W3C credential templates that can be found
[here](https://credentials.walt.id/).
- **Custom credential types** – credentials that you add to the supported list in the
[issuer metadata](https://docs.walt.id/community-stack/issuer/configurations/config-files/credential-issuer-metadata.md).
For this example, we will issue the default Verifiable University Degree, though the process is the same for other
credentials.
### Example Credential: Verifiable University Degree
```json
{
// The contextual information required for data integrity and 1.api
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://www.w3.org/2018/credentials/examples/v1"
],
"id": "http://example.gov/credentials/3732",
// The credential's unique identifier
"type": [
"VerifiableCredential",
"UniversityDegree"
// Specifies the kind of credential being issued
],
"issuer": {
"id": "did:web:vc.transmute.world"
// The issuer's unique identifier (DID)
},
"issuanceDate": "2020-03-10T04:24:12.164Z",
// When the credential was issued
"credentialSubject": {
// Information about the credential's recipient and the degree earned
"id": "did:example:ebfeb1f712ebc6f1c276e12ec21",
"degree": {
"type": "BachelorDegree",
"name": "Bachelor of Science and Arts"
}
}
}
```
**Note**: Fields like `issuer`, `issuanceDate`, and `credentialSubject.did` are dynamic and will be populated with
actual
data upon issuance.
## Issuing a Credential
In this section, we'll generate a signing key which we store ourselves. For production
environments, we recommend using an external KMS provider for key management due to the enhanced security.
Learn more about the different types of keys and the storage options [here](https://docs.walt.id/community-stack/issuer/key-management/overview.md).
### Step 1: Get a Signing Key & Issuer DID
As the issuer API doesn't store any cryptographic key material by default, you need to provide the key used for signing
the credential in one of the following ways:
- **JWK** – the key is managed by you and provided directly in JWK format.
- **External KMS reference** – a reference object that points to a key stored in a supported external KMS such as
[Hashicorp Vault](https://docs.walt.id/community-stack/issuer/key-management/hashicorp-vault.md) or
[Oracle KMS](https://docs.walt.id/community-stack/issuer/key-management/oci-vault.md).
In a production environment, we recommend using an external KMS provider to secure the key material.
To create keys and a related DID for signing credentials, you can use the `/onboard/issuer` endpoint and choose:
- **Key algorithms**: `ed25519`, `secp256k1`, `secp256r1`, or `RSA`.
- **DID methods**: `did:key`, `did:jwk`, `did:web`, or `did:cheqd`.
Please refer to our [Key Management section](https://docs.walt.id/community-stack/issuer/key-management/overview.md) to learn more about the
different options.
For this guide, we will proceed with generating a JWK ed25519 key and a did:key using the `onboard` endpoint.
#### Create Key & DID
**Option: CURL**
**Endpoint:
** `/onboard/issuer` | [API Reference](https://issuer.demo.walt.id/swagger/index.html#/Issuer%20onboarding/post_onboard_issuer)
**Example Request**
```bash
curl -X 'POST' \
'http://0.0.0.0:7002/onboard/issuer' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"key": {
"backend": "jwk",
"keyType": "secp256r1"
},
"did": {
"method": "jwk"
}
}'
```
**Body**
```json
{
"key": {
"backend": "jwk",
"keyType": "secp256r1"
},
"did": {
"method": "jwk"
}
}
```
**Body Parameters**
- `key`
- `backend`: _String_ - Specifies the storage type of key. It can be `jwk` (manged by you), `TSE` (managed by
Hashicorp
Vault) and others. Learn more about different types [here](https://docs.walt.id/community-stack/issuer/key-management/overview.md).
- `keyType`: _String_ - the algorithm used to generate the key. For local, it can
be ed25519, secp256k1, secp256r1, or RSA. For the other types and the supported algorithms, please
go [here](https://docs.walt.id/community-stack/issuer/key-management/overview.md).
- `did`:
- `method`: _String_ - Specifies the DID method. It can be key, jwk, web, cheqd.
---
**Example Response**
The onboard/issuer endpoint will return an object containing both the generated key in JWK format and the related DID.
```json
{
"issuerKey": {
"type": "jwk",
"jwk": {
"kty": "EC",
"d": "n4F7NylR_6YdB5pMHBkieI5SMry7-EXKrggm8yMg1dU",
"crv": "P-256",
"kid": "nsG9yIDCn5mBrKomGLTUlJPHal47Gdov6l_8Jnq9VJI",
"x": "lL8Yf2PDMnAgyVfNQ-IypgV0pP1_XDFqYKMpy9duuAo",
"y": "Nw0D8VdWMYu6XUBKPiQ7Zo4_rZ-nP-y2dAo1gGbvPys"
}
},
"issuerDid": "did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2Iiwia2lkIjoibnNHOXlJRENuNW1CcktvbUdMVFVsSlBIYWw0N0dkb3Y2bF84Sm5xOVZKSSIsIngiOiJsTDhZZjJQRE1uQWd5VmZOUS1JeXBnVjBwUDFfWERGcVlLTXB5OWR1dUFvIiwieSI6Ik53MEQ4VmRXTVl1NlhVQktQaVE3Wm80X3JaLW5QLXkyZEFvMWdHYnZQeXMifQ"
}
```
**Note:**
As we've used the jwk key type, it's important to note that we need to save the returned values ourselves for future
reference.
The API doesn't save any information about created keys or DIDs.
### Step 2: Issue the Credential
Now, we'll issue a verifiable credential using the key and DID obtained (or any other supported JWK).
You can choose between the following signature types:
- **JWT** – a JSON Web Token representation of the credential.
- **SD-JWT** – a Selective-Disclosure JSON Web Token representation of the credential.
Learn more about Selective Disclosure [here](https://docs.walt.id/concepts/selective-disclosure/sd-jwt/intro). In short, it enables your
users to only reveal a subset of the claims in a credential to a verifier when suitable. This increases privacy and
reduces the risk of identity theft and other types of fraud.
To facilitate the issuance of the credential from us (the issuer) to the holder, we will utilise the OID4VCI protocol.
In particular, we will be generating an OID4VC offer URL that can be accepted by any OID compliant wallet to receive
credential(s).
**Note:**
The credential offer URL specifies the credentials to be issued. This includes details such as
the URL of the issuer, the authorisation and token endpoints, and information about the credential's format, type, and
scope.
When we execute the issuance command, two things will happen:
1. The credential will be signed with the provided key and the chosen signature type (JWT or SD-JWT).
2. The information about the signed credential will be embedded into the OID Credential Offer URL, which we can send to
our users so they can claim the credential(s).
**Option: JWT**
[API Reference](https://issuer.demo.walt.id/swagger/index.html#/Credential%20Issuance/post_openid4vc_jwt_issue)
**Option: CURL**
**Endpoint:** `/openid4vc/jwt/issue`
**Example Request**
```bash
curl -X 'POST' \
'https://issuer.demo.walt.id/openid4vc/jwt/issue' \
-H 'accept: text/plain' \
-H 'statusCallbackUri: https://example.com/$id' \
-H 'Content-Type: application/json' \
-d '{
"issuerKey": {
"type": "jwk",
"jwk": {
"kty": "OKP",
"d": "JvJIpga2GD8LJeRu4Sv-mL4thE31DuFlr9PA04CIoZY",
"crv": "Ed25519",
"kid": "iJMS5bkZVIlncfq_Lf_SuxJ2JtQ5Hvaz7tWPnAjUUds",
"x": "FZdvwC8aGhRwqzWptej0NZgtwYAI1SyFg1mKDETOfqE"
}
},
"issuerDid": "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoiaUpNUzVia1pWSWxuY2ZxX0xmX1N1eEoySnRRNUh2YXo3dFdQbkFqVVVkcyIsIngiOiJGWmR2d0M4YUdoUndxeldwdGVqME5aZ3R3WUFJMVN5RmcxbUtERVRPZnFFIn0",
"credentialConfigurationId": "UniversityDegree_jwt_vc_json",
"credentialData": {
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://www.w3.org/2018/credentials/examples/v1"
],
"id": "http://example.gov/credentials/3732",
"type": [
"VerifiableCredential",
"UniversityDegree"
],
"issuer": {
"id": "did:web:vc.transmute.world"
},
"issuanceDate": "2020-03-10T04:24:12.164Z",
"credentialSubject": {
"id": "did:example:ebfeb1f712ebc6f1c276e12ec21",
"degree": {
"type": "BachelorDegree",
"name": "Bachelor of Science and Arts"
}
}
},
"mapping": {
"id": "",
"issuer": {
"id": ""
},
"credentialSubject": {
"id": ""
},
"issuanceDate": "",
"expirationDate": ""
},
"authenticationMethod": "PRE_AUTHORIZED",
"standardVersion": "DRAFT13"
}'
```
**Header Parameters**
- `statusCallbackUri`: _URL_ - Receive updates on the created issuance process, e.g. when a credential was successfully
claimed. The parameter expects a URL which can accept a JSON POST request. The URL can also hold a `$id`, which will
be
replaced by the issuance session id. For example: `https://myurl.com/$id`, `https://myurl.com`
or `https://myurl.com/test/$id`
Expand To Learn More
**Body**
The data send to the provided URL will contain a JSON body:
- `id` : _String_ - the issuance session id
- `type`: _String_ - the event type
- `data`: _JsonObject_ - the data for the event
**Event Types**
Possible events (event types) and their data are:
- `resolved_credential_offer` with the credential offer as JSON (in our Web Wallet: called when the issuance offer
is
entered into the wallet, but not processing / accepted yet)
- `requested_token` with the issuance request for the token as json object (called for the token required to receive
the
credentials)
Credential issuance (called for every credential that's issued (= requested from wallet))
- `jwt_issue` with `jwt` being the issued jwt
- `sdjwt_issue` with `sdjwt` being the issued sdjwt
- `batch_jwt_issue` with `jwt` being the issued jwt
- `batch_sdjwt_issue` with `sdjwt` being the issued sdjwt
- `generated_mdoc` with `mdoc` being the CBOR (HEX) of the signed mdoc
To allow for secure business logic flows, if a callback URL is set, and it cannot be reached, the issuance will not
commence further (after that point). If no callback URL is set, the issuance logic does not change in any way.
**Body**
```json
{
"issuerKey": {
"type": "jwk",
"jwk": {
"kty": "OKP",
"d": "JvJIpga2GD8LJeRu4Sv-mL4thE31DuFlr9PA04CIoZY",
"crv": "Ed25519",
"kid": "iJMS5bkZVIlncfq_Lf_SuxJ2JtQ5Hvaz7tWPnAjUUds",
"x": "FZdvwC8aGhRwqzWptej0NZgtwYAI1SyFg1mKDETOfqE"
}
},
"issuerDid": "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoiaUpNUzVia1pWSWxuY2ZxX0xmX1N1eEoySnRRNUh2YXo3dFdQbkFqVVVkcyIsIngiOiJGWmR2d0M4YUdoUndxeldwdGVqME5aZ3R3WUFJMVN5RmcxbUtERVRPZnFFIn0",
"credentialConfigurationId": "UniversityDegree_jwt_vc_json",
"credentialData": {
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://www.w3.org/2018/credentials/examples/v1"
],
"id": "http://example.gov/credentials/3732",
"type": [
"VerifiableCredential",
"UniversityDegree"
],
"issuer": {
"id": "did:web:vc.transmute.world"
},
"issuanceDate": "2020-03-10T04:24:12.164Z",
"credentialSubject": {
"id": "did:example:ebfeb1f712ebc6f1c276e12ec21",
"degree": {
"type": "BachelorDegree",
"name": "Bachelor of Science and Arts"
}
}
},
"mapping": {
"id": "",
"issuer": {
"id": ""
},
"credentialSubject": {
"id": ""
},
"issuanceDate": "",
"expirationDate": ""
},
"authenticationMethod": "PRE_AUTHORIZED",
"standardVersion": "DRAFT13"
}
```
**Body Parameters**
- `issuerKey`: _JSON_ - A JWK or reference object to a key stored in an external KMS to sign the credential with.
Supported algorithms: ed25519, secp256k1, secp256r1, or
RSA.
**JWK Format:** `{"type": "jwk", "jwk": Here JSON Web Key Object}`. Must be provided as JWK object to "issuerKey".
**KMS Key**: Please refer to the [Key Management Section](https://docs.walt.id/community-stack/issuer/key-management/overview.md) and the KMS you want to
use for more details on the structure
of the reference object.
- `issuerDid`: _String_ - The DID related to the provided key.
- `credentialConfigurationId`: _String_ - Reference to a specific credential configuration the issuer supports. As our
issuer currently supports W3C JWT & SD-JWT credentials and SD-JWT VCs (IETF). The structure of the
credentialConfigurationId is the following: "
credentialType_jwt_vc_json" for W3C JWT & SD-JWT credentials
and "credentialType_vc+sd-jwt" for SD-JWT VCs (IETF). E.g. "UniversityDegree_jwt_vc_json" or "
UniversityDegree_vc+sd-jwt".
You can also view
the credentialConfigurationIds by visiting the `/.well-known/openid-credential-issuer` endpoint of your issuer.
The metadata of our deployed issuer for testing can be
seen [here](https://issuer.demo.walt.id/swagger/index.html#/oidc/get__standardVersion___well_known_openid_credential_issuer).
- `credentialData`: _JSON_ - A credential data structure to sign (W3C compliant). A list of predefined valid structures
can be
found [here](https://credentials.walt.id/).
- `mapping` (optional): _JSON_ - The mapping object that allows for **dynamic value insertion via data functions**,
executed at the time when the credentials is claimed. This feature enables personalized credentials based on real-time
data. Learn more about it and see a list of supported data functions [here](https://docs.walt.id/community-stack/issuer/data-functions).
- `standardVersion`: (optional) _String_ - Defines which OIDC4VCI standard version is used. The supported standard versions are: `DRAFT13` and `DRAFT11`. If no value is provided, it will default to `DRAFT13`.
- `authenticationMethod`: (optional) _String_ - Defines which OIDC4VC exchange flow is used (pre-auth or full-auth). If
no value is
provided, it will default to pre-auth flow indicated as `PRE_AUTHORIZED`. The parameter options are:
Expand To Learn More About The Options
- `PWD` - used for authorization code flow with username/password authentication with external auth server
- `ID_TOKEN` - used for authorization code flow with id_token authentication. When this method is used an authorization
request is sent to the wallet, which generates and issues the ID_TOKEN using its DID (
Decentralized Identifier). The issuer then verifies the ID_TOKEN to confirm the holder's identity and DID.
- `VP_TOKEN` - used for authorization code flow with vp_token(OIDC4VP). With this method, the receiver of the
credential must be authenticated by presenting the requested credential. The credential which must be presented can
be configured using the `vpRequestedValue` as explained below. The policies which are applied to the presented credential
are 'signature,' 'expired,' and 'not-before.' Learn more about policies [here](https://docs.walt.id/community-stack/verifier/credential-verification/policies/overview.md).
- `NONE` - used for authorization code flow with none authentication method
- `PRE_AUTHORIZED` - used for pre-authorizated code flow
**Additional parameters required for selected auth method:**
**For "ID_TOKEN" and "VP_TOKEN":**
- `useJar`: (Optional) _Boolean_ - used for using JAR OAuth specification in the id/vp_token requests. If omitted, the
default value is true.
**For VP_TOKEN:**
- `vpRequestedValue`: _String_ - Specifies the requested credential type value for the VP token. E.g. "VerifiableId"
- `vpProfile`: (Optional) _String_ - Specifies the profile of the VP request. Available Profiles: DEFAULT: For W3C
OpenID4VP,
ISO_18013_7_MDOC: For MDOC OpenID4VP, EBSIV3: For EBSI V3 Compliant VP. If omitted, the default value is DEFAULT
---
**Example Response**
The issuer endpoint will respond with Credential Offer URL.
**Plain Response**
```
openid-credential-offer://issuer.portal.walt.id/?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fissuer.portal.walt.id%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22jwt_vc_json%22%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegree%22%5D%2C%22credential_definition%22%3A%7B%22%40context%22%3A%5B%22https%3A%2F%2Fwww.w3.org%2F2018%2Fcredentials%2Fv1%22%2C%22https%3A%2F%2Fwww.w3.org%2F2018%2Fcredentials%2Fexamples%2Fv1%22%5D%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegree%22%5D%7D%7D%5D%2C%22grants%22%3A%7B%22authorization_code%22%3A%7B%22issuer_state%22%3A%220431b78c-cd94-4f50-bfdf-e24d436c0cf6%22%7D%2C%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiIwNDMxYjc4Yy1jZDk0LTRmNTAtYmZkZi1lMjRkNDM2YzBjZjYiLCJpc3MiOiJodHRwczovL2lzc3Vlci5wb3J0YWwud2FsdC5pZCIsImF1ZCI6IlRPS0VOIn0.NorG7GtjmA-HXMJfUzU9vfnshcIgFY0oYQb8qJjDfORPoNxuurgySSOIDKFi7Z4XJmC-oJZnM0Nbb0NUd57cDA%22%2C%22user_pin_required%22%3Afalse%7D%7D%7D
```
**Decoded**
```
openid-credential-offer://issuer.portal.walt.id/?
credential_offer=
{
"credential_issuer": "https://issuer.demo.walt.id",
"credentials": [
{
"format": "jwt_vc_json",
"types": [
"VerifiableCredential",
"UniversityDegree"
],
"credential_definition": {
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://www.w3.org/2018/credentials/examples/v1"
],
"types": [
"VerifiableCredential",
"UniversityDegree"
]
}
}
],
"grants": {
"authorization_code": {
"issuer_state": "0431b78c-cd94-4f50-bfdf-e24d436c0cf6"
},
"urn:ietf:params:oauth:grant-type:pre-authorized_code": {
"pre-authorized_code": "eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiIwNDMxYjc4Yy1jZDk0LTRmNTAtYmZkZi1lMjRkNDM2YzBjZjYiLCJpc3MiOiJodHRwczovL2lzc3Vlci5wb3J0YWwud2FsdC5pZCIsImF1ZCI6IlRPS0VOIn0.NorG7GtjmA-HXMJfUzU9vfnshcIgFY0oYQb8qJjDfORPoNxuurgySSOIDKFi7Z4XJmC-oJZnM0Nbb0NUd57cDA",
"user_pin_required": false
}
}
}
```
**Option: SD-JWT**
[API Reference](https://issuer.demo.walt.id/swagger/index.html#/Credential%20Issuance/post_openid4vc_sdjwt_issue)
**Option: CURL**
**Endpoint:** `/openid4vc/sdjwt/issue`
**Example Request**
```bash
curl -X 'POST' \
'https://issuer.demo.walt.id/openid4vc/sdjwt/issue' \
-H 'accept: text/plain' \
-H 'Content-Type: application/json' \
-d '{
"issuerKey": {
"type": "jwk",
"jwk": "{\"kty\":\"OKP\",\"d\":\"mDhpwaH6JYSrD2Bq7Cs-pzmsjlLj4EOhxyI-9DM1mFI\",\"crv\":\"Ed25519\",\"kid\":\"Vzx7l5fh56F3Pf9aR3DECU5BwfrY6ZJe05aiWYWzan8\",\"x\":\"T3T4-u1Xz3vAV2JwPNxWfs4pik_JLiArz_WTCvrCFUM\"}"
},
"issuerDid": "did:key:z6MkjoRhq1jSNJdLiruSXrFFxagqrztZaXHqHGUTKJbcNywp",
"credentialConfigurationId": "UniversityDegree_jwt_vc_json",
"credentialData": {
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://www.w3.org/2018/credentials/examples/v1"
],
"id": "http://example.gov/credentials/3732",
"type": [
"VerifiableCredential",
"UniversityDegree"
],
"issuer": {
"id": "did:web:vc.transmute.world"
},
"issuanceDate": "2020-03-10T04:24:12.164Z",
"credentialSubject": {
"id": "did:example:ebfeb1f712ebc6f1c276e12ec21",
"degree": {
"type": "BachelorDegree",
"name": "Bachelor of Science and Arts"
}
}
},
"mapping": {
"id": "",
"issuer": {
"id": ""
},
"credentialSubject": {
"id": ""
},
"issuanceDate": "",
"expirationDate": ""
},
"authenticationMethod": "PRE_AUTHORIZED",
"selectiveDisclosure": {
"fields": {
"issuanceDate": {
"sd": true
},
"credentialSubject": {
"sd": false,
"children": {
"fields": {
"degree": {
"sd": false,
"children": {
"fields": {
"name": {
"sd": true
}
}
}
}
}
}
}
}
}
}'
```
**Body**
```json
{
"issuerKey": {
"type": "jwk",
"jwk": "{\"kty\":\"OKP\",\"d\":\"mDhpwaH6JYSrD2Bq7Cs-pzmsjlLj4EOhxyI-9DM1mFI\",\"crv\":\"Ed25519\",\"kid\":\"Vzx7l5fh56F3Pf9aR3DECU5BwfrY6ZJe05aiWYWzan8\",\"x\":\"T3T4-u1Xz3vAV2JwPNxWfs4pik_JLiArz_WTCvrCFUM\"}"
},
"issuerDid": "did:key:z6MkjoRhq1jSNJdLiruSXrFFxagqrztZaXHqHGUTKJbcNywp",
"credentialConfigurationId": "UniversityDegree_jwt_vc_json",
"credentialData": {
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://www.w3.org/2018/credentials/examples/v1"
],
"id": "http://example.gov/credentials/3732",
"type": [
"VerifiableCredential",
"UniversityDegree"
],
"issuer": {
"id": "did:web:vc.transmute.world"
},
"issuanceDate": "2020-03-10T04:24:12.164Z",
"credentialSubject": {
"id": "did:example:ebfeb1f712ebc6f1c276e12ec21",
"degree": {
"type": "BachelorDegree",
"name": "Bachelor of Science and Arts"
}
}
},
"mapping": {
"id": "",
"issuer": {
"id": ""
},
"credentialSubject": {
"id": ""
},
"issuanceDate": "",
"expirationDate": ""
},
"authenticationMethod": "PRE_AUTHORIZED",
"selectiveDisclosure": {
"fields": {
"issuanceDate": {
"sd": true
},
"credentialSubject": {
"sd": false,
"children": {
"fields": {
"degree": {
"sd": false,
"children": {
"fields": {
"name": {
"sd": true
}
}
}
}
}
}
}
}
}
}
```
**Body Parameters**
- `issuerKey`: _JSON_ - A JWK or reference object to a key stored in an external KMS to sign the credential with.
Supported algorithms: ed25519, secp256k1, secp256r1, or
RSA.
**JWK Format:** `{"type": "jwk", "jwk": "Here JSON Web Key Object"}`. Please make sure to escape the JWK object as
a string when providing as a value to "issuerKey".
**KMS Key**: Please refer to the [Key Management Section](https://docs.walt.id/community-stack/issuer/key-management/overview.md) and the KMS you want to
use for more details on the structure
of the reference object.
- `issuerDid`: _String_ - The DID related to the provided key.
- `credentialConfigurationId`: _String_ - Reference to a specific credential configuration the issuer supports. As our
issuer currently supports W3C JWT & SD-JWT credentials and SD-JWT VCs (IETF). The structure of the
credentialConfigurationId is the following: "
credentialType_jwt_vc_json" for W3C JWT & SD-JWT credentials
and "credentialType_vc+sd-jwt" for SD-JWT VCs (IETF). E.g. "UniversityDegree_jwt_vc_json" or "
UniversityDegree_vc+sd-jwt".
You can also view
the credentialConfigurationIds by visiting the `/.well-known/openid-credential-issuer` endpoint of your issuer.
The metadata of our deployed issuer for testing can be
seen [here](https://issuer.demo.walt.id/swagger/index.html#/oidc/get__standardVersion___well_known_openid_credential_issuer).
- `credentialData`: _JSON_ - A credential data structure to sign (W3C compliant). A list of predefined valid structures
can be
found [here](https://credentials.walt.id/).
- `mapping` (optional): _JSON_ - The mapping object that allows for **dynamic value insertion via data functions**,
executed at the time when the credentials is claimed. This feature enables personalized credentials based on real-time
data. Learn more about it and see a list of supported data functions [here](https://docs.walt.id/community-stack/issuer/credential-issuance/data-functions.md).
- `authenticationMethod`: (optional) _String_ - Defines which OIDC4VC exchange flow is used (pre-auth or full-auth). If
no value is
provided, it will default to pre-auth flow indicated as `PRE_AUTHORIZED`. The parameter options are:
Expand To Learn More About The Options
- `PWD` - used for authorization code flow with username/password authentication with external auth server
- `ID_TOKEN` - used for authorization code flow with id_token authentication. When this method is used an authorization
request is sent to the wallet, which generates and issues the ID_TOKEN using its DID (
Decentralized Identifier). The issuer then verifies the ID_TOKEN to confirm the holder's identity and DID.
- `VP_TOKEN` - used for authorization code flow with vp_token(OIDC4VP). With this method, the receiver of the
credential must be authenticated by presenting the requested credential. The credential which must be presented can
be configured using the `vpRequestedValue` as explained below. The policies which are applied to the presented credential
are 'signature,' 'expired,' and 'not-before.' Learn more about policies [here](https://docs.walt.id/community-stack/verifier/credential-verification/policies/overview.md).
- `NONE` - used for authorization code flow with none authentication method
- `PRE_AUTHORIZED` - used for pre-authorizated code flow
**Additional parameters required for selected auth method:**
**For "ID_TOKEN" and "VP_TOKEN":**
- `useJar`: (Optional) _Boolean_ - used for using JAR OAuth specification in the id/vp_token requests. If omitted, the
default value is true.
**For VP_TOKEN:**
- `vpRequestedValue`: _String_ - Specifies the requested credential type value for the VP token. E.g. "VerifiableId"
- `vpProfile`: (Optional) _String_ - Specifies the profile of the VP request. Available Profiles: DEFAULT: For W3C
OpenID4VP,
ISO_18013_7_MDOC: For MDOC OpenID4VP, EBSIV3: For EBSI V3 Compliant VP. If omitted, the default value is DEFAULT
- [`selectiveDisclosure`](https://docs.walt.id/concepts/selective-disclosure/intro) (optional): _JSON_ -An object that configures which
claims in the credential should be
selectively disclosable. It's manged through the following properties:
- `fields`: An object illustrating the hierarchical structure of the credential contents. Each key in this object
specifies the name of the fields in the credential. Every field, whether high-level or nested, can be represented
by an object with a "sd" key. If the value for "sd" is set to true, it means that the corresponding field is
selectively disclosable. Moreover, fields that have nested attributes are represented with a "children" key which
contains another fields object reflecting the structure of the nested object. For example:
```json
{
"fields": {
"issuanceDate": {
"sd": true
},
"credentialSubject": {
"sd": false,
"children": {
"fields": {
"degree": {
"sd": false,
"children": {
"fields": {
"name": {
"sd": true
}
}
}
}
}
}
}
}
}
```
**Example Response**
The issuer endpoint will respond with Credential Offer URL.
**Plain Response**
```
openid-credential-offer://issuer.portal.walt.id/?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fissuer.portal.walt.id%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22jwt_vc_json%22%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegree%22%5D%2C%22credential_definition%22%3A%7B%22%40context%22%3A%5B%22https%3A%2F%2Fwww.w3.org%2F2018%2Fcredentials%2Fv1%22%2C%22https%3A%2F%2Fwww.w3.org%2F2018%2Fcredentials%2Fexamples%2Fv1%22%5D%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegree%22%5D%7D%7D%5D%2C%22grants%22%3A%7B%22authorization_code%22%3A%7B%22issuer_state%22%3A%22280b7b66-b8ba-420e-91f1-f3bb684d3edf%22%7D%2C%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiIyODBiN2I2Ni1iOGJhLTQyMGUtOTFmMS1mM2JiNjg0ZDNlZGYiLCJpc3MiOiJodHRwczovL2lzc3Vlci5wb3J0YWwud2FsdC5pZCIsImF1ZCI6IlRPS0VOIn0.H0HUMNQ5jSzrNEkaVyKlwK4yDq8_sOzRZuQ-2nCwFKMsTrFzEXj95lsgo0Av5RhnOIrhBd98jGjZ0DDwEONwDg%22%2C%22user_pin_required%22%3Afalse%7D%7D%7D
```
**Decoded**
```
openid-credential-offer://issuer.portal.walt.id/?credential_offer={
"credential_issuer": "https://issuer.demo.walt.id",
"credentials": [
{
"format": "jwt_vc_json",
"types": [
"VerifiableCredential",
"UniversityDegree"
],
"credential_definition": {
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://www.w3.org/2018/credentials/examples/v1"
],
"types": [
"VerifiableCredential",
"UniversityDegree"
]
}
}
],
"grants": {
"authorization_code": {
"issuer_state": "280b7b66-b8ba-420e-91f1-f3bb684d3edf"
},
"urn:ietf:params:oauth:grant-type:pre-authorized_code": {
"pre-authorized_code": "eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiIyODBiN2I2Ni1iOGJhLTQyMGUtOTFmMS1mM2JiNjg0ZDNlZGYiLCJpc3MiOiJodHRwczovL2lzc3Vlci5wb3J0YWwud2FsdC5pZCIsImF1ZCI6IlRPS0VOIn0.H0HUMNQ5jSzrNEkaVyKlwK4yDq8_sOzRZuQ-2nCwFKMsTrFzEXj95lsgo0Av5RhnOIrhBd98jGjZ0DDwEONwDg",
"user_pin_required": false
}
}
}
```
When you receive the credential using our web wallet and make a presentation to a verifier, you will see that before
accepting the verification request, you will have the option to select which field(s) to share that were marked as
selectively disclosable.
### Step 3: Receive the Credential Offer
The created credential offer can now be:
- **Embedded into a QR code** for users to scan with their mobile wallet.
- **Pasted manually** into the credential offer field of our
[web wallet](https://wallet.demo.walt.id/).
**Try It Out**: Use our [web wallet](https://wallet.demo.walt.id/) for a practical demonstration:
1. Log in.
2. Click the `request credential` button.
3. Paste the received Offer URL into the text field below the camera.
🎉 Congratulations, you've issued a W3C-compliant Verifiable Credential using OID4VCI! 🎉
To learn how to accept this credential offer programmatically using the Wallet API, see [How to Accept W3C Verifiable Credentials via OID4VCI](https://docs.walt.id/community-stack/wallet/credential-exchange/guides/accept-w3c-vc-oid4vci.md).
--------------------------------------------------------------------------------
PAGE: How to Issue SD-JWT-based Verifiable Credentials (IETF | SD-JWT VC) via OID4VCI with walt.id
URL: https://docs.walt.id/community-stack/issuer/credential-issuance/sd-jwt-vc-oid4vc
RAW: https://docs.walt.id/community-stack/issuer/credential-issuance/sd-jwt-vc-oid4vc.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# How to Issue SD-JWT-based Verifiable Credentials (IETF | SD-JWT VC) via OID4VCI with walt.id
**TL;DR**
**What you’ll learn:**
- Generate issuer keys and DIDs with the `/onboard/issuer` endpoint to sign SD-JWT VCs.
- Issue SD-JWT VCs over OID4VCI using RAW keys or keys managed via an external KMS.
- Configure selective disclosure fields, mapping objects, and authentication methods for issuance sessions.
- Create credential offer URLs that any compliant wallet can redeem for a credential.
**Relevant concepts:**
- [SD-JWT VC](https://docs.walt.id/concepts/digital-credentials/sd-jwt-vc.md) – IETF SD-JWT VCs standard.
- [OID4VCI](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md) – protocol for issuing verifiable credentials to wallets.
Video: https://youtu.be/cnxEljh8eyU
This guide provides a comprehensive walkthrough for issuing an SD-JWT VC credential based on
the [IETF standard](https://datatracker.ietf.org/doc/draft-ietf-oauth-sd-jwt-vc/)
using the walt.id issuer API. If you are interested to learn how to issue an W3C Verifiable Credentials, please
go [here](https://docs.walt.id/community-stack/issuer/credential-issuance/vc-oid4vc.md).
The issuance process will utilize the
[OID4VCI protocol](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-ID1.html).
## Setup
See how to access to the issuer API below.
- [Deployed (Testing Only)](https://issuer.demo.walt.id/swagger/index.html) - Use our deployed version for testing.
- [Local](https://docs.walt.id/community-stack/issuer/setup.md) - Run the API in your environment with our open-source setup.
## Preparing for Issuance: Key Components
Before issuing a verifiable credential, you'll need:
1. **Raw Credential Data**: The information you want to issue as a VC, typically following a specific template or
schema.
2. **Issuer [**Decentralized Identifier (DID)**](https://docs.walt.id/concepts/decentralised-identifiers.md) or X.509 Certificate**: When issuing a SD-JWT VC credential, it can either be signed using the key of an
issuer DID or a key which is trusted by an X.509 certificate.
The walt.id issuer API supports SD-JWT VC credentials in two ways:
- **Default credential types** – available out of the box and listed in the credential issuer metadata endpoint, e.g. [here](https://issuer.demo.walt.id/swagger/index.html#/oidc/get__well_known_openid_credential_issuer).
- **Custom credentials** – added by extending the supported credentials in the [issuer metadata](https://docs.walt.id/community-stack/issuer/configurations/config-files/credential-issuer-metadata.md).
For this example, we will issue the default supported Identity Credential, though the process is the same for other credentials.
### Example ID Credential:
```json
{
"vct": "identity_credential_vc+sd-jwt",
"given_name": "John",
"family_name": "Doe",
"email": "johndoe@example.com",
"phone_number": "+1-202-555-0101",
"address": {
"street_address": "123 Main St",
"locality": "Anytown",
"region": "Anystate",
"country": "US"
},
"birthdate": "1940-01-01",
"is_over_18": true,
"is_over_21": true,
"is_over_65": true
}
```
## Issuing a Credential
Choose below if you want to sign the credential using the key of a DID or an X.509 certificate.
**Option: DID**
We will now generate a key and associated DID via the issuer API. However, we need to store
the key material + DID ourselves.
For production environments, we recommend using an external KMS provider for key management due to the
enhanced security. Learn more about the different types of keys and the storage options [here](https://docs.walt.id/community-stack/issuer/key-management/overview.md).
### Step 1: Get a Signing Key & Issuer DID
As the issuer API doesn't store any cryptographic key material by default, you need to manage and provide the key used for signing the credential. You can:
- **Use a JWK key** – provide the signing key directly in JWK format and manage it yourself.
- **Use an external KMS** – provide a reference object that points to a key stored in a supported KMS such as [Hashicorp Vault](https://docs.walt.id/community-stack/issuer/key-management/hashicorp-vault.md) or [Oracle KMS](https://docs.walt.id/community-stack/issuer/key-management/oci-vault.md).
In a production environment, we recommend using an external KMS provider to secure the key material.
To create keys for signing credentials,
you can use the `/onboard/issuer` endpoint and choose from a variety of algorithms (ed25519,
secp256k1, secp256r1, or RSA). The DID generated by the endpoint is not needed for the SD-JWT VC.
Please refer to our [Key Management section](https://docs.walt.id/community-stack/issuer/key-management/overview.md)
to learn more about the different options.
For this guide, we will proceed with generating a JWK Ed25519 key using the `onboard` endpoint.
#### Create Key
**Option: CURL**
**Endpoint:** `/onboard/issuer` | [API Reference](https://issuer.demo.walt.id/swagger/index.html#/Issuer%20onboarding/post_onboard_issuer)
**Example Request**
```bash
curl -X 'POST' \
'http://0.0.0.0:7002/onboard/issuer' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"key": {
"backend": "jwk",
"keyType": "Ed25519"
},
"did": {
"method": "jwk"
}
}'
```
**Body**
```json
{
"key": {
"backend": "jwk",
"keyType": "Ed25519"
},
"did": {
"method": "jwk"
}
}
```
**Body Parameters**
- `key`
- `backend`: _String_ - Specifies the storage type of key. It can be `jwk` (manged by you), `TSE` (managed by
Hashicorp
Vault) and others. Learn more about different types [here](https://docs.walt.id/community-stack/issuer/key-management/overview.md).
- `keyType`: _String_ - the algorithm used to generate the key. For local, it can
be ed25519, secp256k1, secp256r1, or RSA. For the other types and the supported algorithms, please
go [here](https://docs.walt.id/community-stack/issuer/key-management/overview.md).
- `did`:
- `method`: _String_ - Specifies the DID method. It can be key, jwk, web, cheqd.
---
**Example Response**
The onboard/issuer endpoint will return an object containing both the generated key in JWK format and the related DID.
```json
{
"issuerKey": {
"type": "jwk",
"jwk": {
"kty": "EC",
"d": "n4F7NylR_6YdB5pMHBkieI5SMry7-EXKrggm8yMg1dU",
"crv": "P-256",
"kid": "nsG9yIDCn5mBrKomGLTUlJPHal47Gdov6l_8Jnq9VJI",
"x": "lL8Yf2PDMnAgyVfNQ-IypgV0pP1_XDFqYKMpy9duuAo",
"y": "Nw0D8VdWMYu6XUBKPiQ7Zo4_rZ-nP-y2dAo1gGbvPys"
}
},
"issuerDid": "did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2Iiwia2lkIjoibnNHOXlJRENuNW1CcktvbUdMVFVsSlBIYWw0N0dkb3Y2bF84Sm5xOVZKSSIsIngiOiJsTDhZZjJQRE1uQWd5VmZOUS1JeXBnVjBwUDFfWERGcVlLTXB5OWR1dUFvIiwieSI6Ik53MEQ4VmRXTVl1NlhVQktQaVE3Wm80X3JaLW5QLXkyZEFvMWdHYnZQeXMifQ"
}
```
**Important:** As we've used the jwk key type, it's important to note that we need to save the returned values ourselves for future
reference.
The API doesn't save any information about created keys.
### Step 2: Issue the Credential
To issue a verifiable credential, we will use the obtained key or any other supported JWK. When issuing an SD-JWT
VC credential, we have the option to selectively disclose certain claims in the credential. By default, all claims are
visible. The configuration for selectively disclosing claims can be done through a configuration object outlined below.
To facilitate the issuance of the credential from us (the issuer) to the holder, we will utilise the OID4VCI protocol.
In particular, we will be generating an OID4VC offer URL that can be accepted by any OID compliant wallet to receive
credential(s).
**Note:**
The credential offer URL specifies the credentials to be issued. This includes details such as
the URL of the issuer, the authorisation and token endpoints, and information about the credential's format, type, and
scope.
When we execute the issuance command, two things will happen:
1. The credential will be signed with the provided key.
2. Information about the offered credential will be embedded into the OID Credential Offer
URL, which we can send off to our users to claim the credential(s) later on.
**Note:**
The Verifiable Credential Type (VCT) is resolved through the credentialConfigurationId. The `vct` is found as a parameter within the corresponding entry associated with that id.
Issuer API generates proper resolvable urls for VCT based on provided configuration and host endpoints to retrieve type metadata with the scheme `https:///.well-known/vct/` (e.g. `https://issuer.demo.walt.id/.well-known/vct/identity_credential`).
Find more info [here](https://docs.walt.id/concepts/digital-credentials/sd-jwt-vc.md#verifiable-credential-type-and-type-metadata)
**Option: CURL**
**Endpoint:**
`/openid4vc/sdjwt/issue` | [API Reference](https://issuer.demo.walt.id/swagger/index.html#/Credential%20Issuance/post_openid4vc_sdjwt_issue)
**Example Request**
```bash
curl -X 'POST' \
'https://issuer.demo.walt.id/openid4vc/sdjwt/issue' \
-H 'accept: text/plain' \
-H 'statusCallbackUri: https://example.com/$id' \
-H 'Content-Type: application/json' \
-d '{
"issuerKey": {
"type": "jwk",
"jwk": {
"kty": "OKP",
"d": "mDhpwaH6JYSrD2Bq7Cs-pzmsjlLj4EOhxyI-9DM1mFI",
"crv": "Ed25519",
"kid": "Vzx7l5fh56F3Pf9aR3DECU5BwfrY6ZJe05aiWYWzan8",
"x": "T3T4-u1Xz3vAV2JwPNxWfs4pik_JLiArz_WTCvrCFUM"
}
},
"issuerDid": "did:key:z6MkjoRhq1jSNJdLiruSXrFFxagqrztZaXHqHGUTKJbcNywp",
"credentialConfigurationId": "identity_credential_vc+sd-jwt",
"credentialData": {
"given_name": "John",
"family_name": "Doe",
"email": "johndoe@example.com",
"phone_number": "+1-202-555-0101",
"address": {
"street_address": "123 Main St",
"locality": "Anytown",
"region": "Anystate",
"country": "US"
},
"birthdate": "1940-01-01",
"is_over_18": true,
"is_over_21": true,
"is_over_65": true
},
"mapping": {
"id": "",
"iat": "",
"nbf": "",
"exp": ""
},
"authenticationMethod": "PRE_AUTHORIZED",
"selectiveDisclosure": {
"fields": {
"birthdate": {
"sd": true
}
},
"decoyMode": "NONE",
"decoys": 0
}
}'
```
**Header Parameters**
- `statusCallbackUri`: _URL_ - Receive updates on the created issuance process, e.g. when a credential was successfully
claimed. The parameter expects a URL which can accept a JSON POST request. The URL can also hold a `$id`, which will
be
replaced by the issuance session id. For example: `https://myurl.com/$id`, `https://myurl.com`
or `https://myurl.com/test/$id`
Expand To Learn More
**Body**
The data send to the provided URL will contain a JSON body:
- `id` : _String_ - the issuance session id
- `type`: _String_ - the event type
- `data`: _JsonObject_ - the data for the event
**Event Types**
Possible events (event types) and their data are:
- `resolved_credential_offer` with the credential offer as JSON (in our Web Wallet: called when the issuance offer is
entered into the wallet, but not processing / accepted yet)
- `requested_token` with the issuance request for the token as json object (called for the token required to receive the
credentials)
Credential issuance (called for every credential that's issued (= requested from wallet))
- `jwt_issue` with `jwt` being the issued jwt
- `sdjwt_issue` with `sdjwt` being the issued sdjwt
- `batch_jwt_issue` with `jwt` being the issued jwt
- `batch_sdjwt_issue` with `sdjwt` being the issued sdjwt
- `generated_mdoc` with `mdoc` being the CBOR (HEX) of the signed mdoc
To allow for secure business logic flows, if a callback URL is set, and it cannot be reached, the issuance will not
commence further (after that point). If no callback URL is set, the issuance logic does not change in any way.
**Body**
```json
{
"issuerKey": {
"type": "jwk",
"jwk": {
"kty": "OKP",
"d": "mDhpwaH6JYSrD2Bq7Cs-pzmsjlLj4EOhxyI-9DM1mFI",
"crv": "Ed25519",
"kid": "Vzx7l5fh56F3Pf9aR3DECU5BwfrY6ZJe05aiWYWzan8",
"x": "T3T4-u1Xz3vAV2JwPNxWfs4pik_JLiArz_WTCvrCFUM"
}
},
"issuerDid": "did:key:z6MkjoRhq1jSNJdLiruSXrFFxagqrztZaXHqHGUTKJbcNywp",
"credentialConfigurationId": "identity_credential_vc+sd-jwt",
"credentialData": {
"given_name": "John",
"family_name": "Doe",
"email": "johndoe@example.com",
"phone_number": "+1-202-555-0101",
"address": {
"street_address": "123 Main St",
"locality": "Anytown",
"region": "Anystate",
"country": "US"
},
"birthdate": "1940-01-01",
"is_over_18": true,
"is_over_21": true,
"is_over_65": true
},
"mapping": {
"id": "",
"iat": "",
"nbf": "",
"exp": ""
},
"authenticationMethod": "PRE_AUTHORIZED",
"selectiveDisclosure": {
"fields": {
"birthdate": {
"sd": true
}
},
"decoyMode": "NONE",
"decoys": 0
}
}
```
**Body Parameters**
- `issuerKey`: _JSON_ - A JWK or reference object to a key stored in an external KMS to sign the credential with.
Supported algorithms: ed25519, secp256k1, secp256r1, or
RSA.
**JWK Format:** `{"type": "jwk", "jwk": Here JSON Web Key Object}`. Can be provided as String Or JWK object to "
issuerKey".
**KMS Key**: Please refer to the [Key Management Section](https://docs.walt.id/community-stack/issuer/key-management/overview.md) and the KMS you want to
use for more details on the structure
of the reference object.
- `credentialConfigurationId`: _String_ - Reference to a specific credential configuration the issuer supports. As our
issuer currently supports W3C JWT & SD-JWT credentials and SD-JWT VCs (IETF). The structure of the credentialConfigurationId is the following: "
credentialType_jwt_vc_json" for W3C JWT & SD-JWT credentials
and "credentialType_vc+sd-jwt" for SD-JWT VCs (IETF). E.g. "UniversityDegree_jwt_vc_json" or "UniversityDegree_vc+sd-jwt".
You can also view
the credentialConfigurationIds by visiting the `/.well-known/openid-credential-issuer` endpoint of your issuer.
The metadata of our deployed issuer for testing can be seen [here](https://issuer.demo.walt.id/swagger/index.html#/oidc/get__standardVersion___well_known_openid_credential_issuer).
- `credentialData`: _JSON_ - Credential data structure to sign.
- `mapping` (optional): _JSON_ - The mapping object that allows for **dynamic value insertion via data functions**,
executed at the time when the credentials is claimed. This feature enables personalized credentials based on real-time
data. Learn more about it and see a list of supported data functions [here](https://docs.walt.id/community-stack/issuer/data-functions).
In case of IETF SD-JWT, the values could be `id`, `iat`, `nbf` and `exp`.
- `authenticationMethod`: (optional) _String_ - Defines which OIDC4VC exchange flow is used (pre-auth or full-auth). If
no value is
provided, it will default to pre-auth flow indicated as `PRE_AUTHORIZED`. The parameter options are:
Expand To Learn More About The Options
- `PWD` - used for authorization code flow with username/password authentication with external auth server
- `ID_TOKEN` - used for authorization code flow with id_token authentication. When this method is used an authorization
request is sent to the wallet, which generates and issues the ID_TOKEN using its DID (
Decentralized Identifier). The issuer then verifies the ID_TOKEN to confirm the holder's identity and DID.
- `VP_TOKEN` - used for authorization code flow with vp_token(OIDC4VP). With this method, the receiver of the
credential must be authenticated by presenting the requested credential. The credential which must be presented can
be configured using the `vpRequestedValue` as explained below. The policies which are applied to the presented credential
are 'signature,' 'expired,' and 'not-before.' Learn more about policies [here](https://docs.walt.id/community-stack/verifier/credential-verification/policies/overview.md).
- `NONE` - used for authorization code flow with none authentication method
- `PRE_AUTHORIZED` - used for pre-authorizated code flow
**Additional parameters required for selected auth method:**
**For "ID_TOKEN" and "VP_TOKEN":**
- `useJar`: (Optional) _Boolean_ - used for using JAR OAuth specification in the id/vp_token requests. If omitted, the
default value is true.
**For VP_TOKEN:**
- `vpRequestedValue`: _String_ - Specifies the requested credential type value for the VP token. E.g. "VerifiableId"
- `vpProfile`: (Optional) _String_ - Specifies the profile of the VP request. Available Profiles: DEFAULT: For W3C
OpenID4VP,
ISO_18013_7_MDOC: For MDOC OpenID4VP, EBSIV3: For EBSI V3 Compliant VP. If omitted, the default value is DEFAULT
- [`selectiveDisclosure`](https://docs.walt.id/concepts/selective-disclosure/intro) (optional): _JSON_ -An object that configures which
claims in the credential should be
selectively disclosable. It's manged through the following properties:
Expand To Learn More
- `fields`: An object illustrating the hierarchical structure of the credential contents. Each key in this object
specifies the name of the fields in the credential. Every field, whether high-level or nested, can be represented
by an object with a "sd" key. If the value for "sd" is set to true, it means that the corresponding field is
selectively disclosable. Moreover, fields that have nested attributes are represented with a "children" key which
contains another fields object reflecting the structure of the nested object. For example:
```json
{
"fields": {
"issuanceDate": {
"sd": true
},
"credentialSubject": {
"sd": false,
"children": {
"fields": {
"degree": {
"sd": false,
"children": {
"fields": {
"name": {
"sd": true
}
}
}
}
}
}
}
}
}
```
---
**Example Response**
The issuer endpoint will respond with Credential Offer URL.
**Plain Response**
```
openid-credential-offer://issuer.potential.walt-test.cloud/?credential_offer_uri=https%3A%2F%2Fissuer.potential.walt-test.cloud%2Fopenid4vc%2FcredentialOffer%3Fid%3D9aabdb65-defe-464b-baa0-9cc13b36074a
```
**Decoded**
```
{
"credential_issuer": "https://issuer.potential.walt-test.cloud",
"credential_configuration_ids": [
"identity_credential_vc+sd-jwt"
],
"grants": {
"authorization_code": {
"issuer_state": "9aabdb65-defe-464b-baa0-9cc13b36074a"
},
"urn:ietf:params:oauth:grant-type:pre-authorized_code": {
"pre-authorized_code": "eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiI5YWFiZGI2NS1kZWZlLTQ2NGItYmFhMC05Y2MxM2IzNjA3NGEiLCJpc3MiOiJodHRwczovL2lzc3Vlci5wb3RlbnRpYWwud2FsdC10ZXN0LmNsb3VkIiwiYXVkIjoiVE9LRU4ifQ.vl9fBjoYUqr1nrA0jZ3ZjpS45yHLp2roMPCxCoqjuLNyBhTGO0g_PXMw8_NWhD-3qllRq5J0kLAw8WmfvT95Cw"
}
}
}
```
**Option: x.509 certificate**
In this section we will use an x.509 certificate to sign and issue the credential. As our credential is in the SD-JWT
VC format, we have the option to selectively disclose certain claims in the credential. By default, all claims are
visible. The configuration for selectively disclosing claims can be done through a configuration object outlined below.
To facilitate the issuance of the credential from us (the issuer) to the holder, we will utilise the OID4VCI protocol.
In particular, we will be generating an OID4VC offer URL that can be accepted by any OID compliant wallet to receive
credential(s).
**Note:**
The credential offer URL specifies the credentials to be issued. This includes details such as
the URL of the issuer, the authorisation and token endpoints, and information about the credential's format, type, and
scope.
When we execute the issuance command, two things will happen:
1. The credential will be signed with the provided certificate.
2. Information about the offered credential will be embedded into the OID Credential Offer
URL, which we can send off to our users to claim the credential(s) later on.
**Note:**
The Verifiable Credential Type (VCT) is resolved through the credentialConfigurationId. The `vct` is found as a parameter within the corresponding entry associated with that id.
Issuer API generates proper resolvable urls for VCT based on provided configuration and host endpoints to retrieve type metadata with the scheme `https:///.well-known/vct/` (e.g. `https://issuer.demo.walt.id/.well-known/vct/identity_credential`)
Find more info [here](https://docs.walt.id/concepts/digital-credentials/sd-jwt-vc.md#verifiable-credential-type-and-type-metadata)
**Option: CURL**
**Endpoint:**
`/openid4vc/sdjwt/issue` | [API Reference](https://issuer.demo.walt.id/swagger/index.html#/Credential%20Issuance/post_openid4vc_sdjwt_issue)
**Example Request**
```bash
curl -X 'POST' \
'https://issuer.demo.walt.id/openid4vc/sdjwt/issue' \
-H 'accept: text/plain' \
-H 'statusCallbackUri: https://example.com/$id' \
-H 'Content-Type: application/json' \
-d '{
"issuerKey": {
"type": "jwk",
"jwk": {
"kty": "OKP",
"d": "mDhpwaH6JYSrD2Bq7Cs-pzmsjlLj4EOhxyI-9DM1mFI",
"crv": "Ed25519",
"kid": "Vzx7l5fh56F3Pf9aR3DECU5BwfrY6ZJe05aiWYWzan8",
"x": "T3T4-u1Xz3vAV2JwPNxWfs4pik_JLiArz_WTCvrCFUM"
}
},
"credentialConfigurationId": "identity_credential_vc+sd-jwt",
"credentialData": {
"given_name": "John",
"family_name": "Doe",
"email": "johndoe@example.com",
"phone_number": "+1-202-555-0101",
"address": {
"street_address": "123 Main St",
"locality": "Anytown",
"region": "Anystate",
"country": "US"
},
"birthdate": "1940-01-01",
"is_over_18": true,
"is_over_21": true,
"is_over_65": true
},
"mapping": {
"id": "",
"iat": "",
"nbf": "",
"exp": ""
},
"authenticationMethod": "PRE_AUTHORIZED",
"selectiveDisclosure": {
"fields": {
"birthdate": {
"sd": true
}
},
"decoyMode": "NONE",
"decoys": 0
},
"authenticationMethod": "PRE_AUTHORIZED",
"x5Chain": [
"-----BEGIN CERTIFICATE-----\nMIIBRzCB7qADAgECAgg57ch6mnj5KjAKBggqhkjOPQQDAjAXMRUwEwYDVQQDDAxNRE9DIFJPT1QgQ0EwHhcNMjQwNTAyMTMxMzMwWhcNMjUwNTAyMTMxMzMwWjAbMRkwFwYDVQQDDBBNRE9DIFRlc3QgSXNzdWVyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEG0RINBiF+oQUD3d5DGnegQuXenI29JDaMGoMvioKRBN53d4UazakS2unu8BnsEtxutS2kqRhYBPYk9RAriU3gaMgMB4wDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCB4AwCgYIKoZIzj0EAwIDSAAwRQIhAI5wBBAA3ewqIwslhuzFn4rNFW9dkz2TY7xeImO7CraYAiAYhai1NzJ6abAiYg8HxcRdYpO4bu2Sej8E6CzFHK34Yw==\n-----END CERTIFICATE-----"
],
"trustedRootCAs": [
"-----BEGIN CERTIFICATE-----\nMIIBQzCB66ADAgECAgjbHnT+6LsrbDAKBggqhkjOPQQDAjAYMRYwFAYDVQQDDA1NRE9DIFJPT1QgQ1NQMB4XDTI0MDUwMjEzMTMzMFoXDTI0MDUwMzEzMTMzMFowFzEVMBMGA1UEAwwMTURPQyBST09UIENBMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWP0sG+CkjItZ9KfM3sLF+rLGb8HYCfnlsIH/NWJjiXkTx57ryDLYfTU6QXYukVKHSq6MEebvQPqTJT1blZ/xeKMgMB4wDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCAQYwCgYIKoZIzj0EAwIDRwAwRAIgWM+JtnhdqbTzFD1S3byTvle0n/6EVALbkKCbdYGLn8cCICOoSETqwk1oPnJEEPjUbdR4txiNqkHQih8HKAQoe8t5\n-----END CERTIFICATE-----\n"
]
}'
```
**Header Parameters**
- `statusCallbackUri`: _URL_ - Receive updates on the created issuance process, e.g. when a credential was successfully
claimed. The parameter expects a URL which can accept a JSON POST request. The URL can also hold a `$id`, which will
be
replaced by the issuance session id. For example: `https://myurl.com/$id`, `https://myurl.com`
or `https://myurl.com/test/$id`
Expand To Learn More
**Body**
The data send to the provided URL will contain a JSON body:
- `id` : _String_ - the issuance session id
- `type`: _String_ - the event type
- `data`: _JsonObject_ - the data for the event
**Event Types**
Possible events (event types) and their data are:
- `resolved_credential_offer` with the credential offer as JSON (in our Web Wallet: called when the issuance offer is
entered into the wallet, but not processing / accepted yet)
- `requested_token` with the issuance request for the token as json object (called for the token required to receive the
credentials)
Credential issuance (called for every credential that's issued (= requested from wallet))
- `jwt_issue` with `jwt` being the issued jwt
- `sdjwt_issue` with `sdjwt` being the issued sdjwt
- `batch_jwt_issue` with `jwt` being the issued jwt
- `batch_sdjwt_issue` with `sdjwt` being the issued sdjwt
- `generated_mdoc` with `mdoc` being the CBOR (HEX) of the signed mdoc
To allow for secure business logic flows, if a callback URL is set, and it cannot be reached, the issuance will not
commence further (after that point). If no callback URL is set, the issuance logic does not change in any way.
**Body**
```json
{
"issuerKey": {
"type": "jwk",
"jwk": {
"kty": "OKP",
"d": "mDhpwaH6JYSrD2Bq7Cs-pzmsjlLj4EOhxyI-9DM1mFI",
"crv": "Ed25519",
"kid": "Vzx7l5fh56F3Pf9aR3DECU5BwfrY6ZJe05aiWYWzan8",
"x": "T3T4-u1Xz3vAV2JwPNxWfs4pik_JLiArz_WTCvrCFUM"
}
},
"credentialConfigurationId": "identity_credential_vc+sd-jwt",
"credentialData": {
"given_name": "John",
"family_name": "Doe",
"email": "johndoe@example.com",
"phone_number": "+1-202-555-0101",
"address": {
"street_address": "123 Main St",
"locality": "Anytown",
"region": "Anystate",
"country": "US"
},
"birthdate": "1940-01-01",
"is_over_18": true,
"is_over_21": true,
"is_over_65": true
},
"mapping": {
"id": "",
"iat": "",
"nbf": "",
"exp": ""
},
"selectiveDisclosure": {
"fields": {
"birthdate": {
"sd": true
}
},
"decoyMode": "NONE",
"decoys": 0
},
"authenticationMethod": "PRE_AUTHORIZED",
"x5Chain": [
"-----BEGIN CERTIFICATE-----\nMIIBRzCB7qADAgECAgg57ch6mnj5KjAKBggqhkjOPQQDAjAXMRUwEwYDVQQDDAxNRE9DIFJPT1QgQ0EwHhcNMjQwNTAyMTMxMzMwWhcNMjUwNTAyMTMxMzMwWjAbMRkwFwYDVQQDDBBNRE9DIFRlc3QgSXNzdWVyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEG0RINBiF+oQUD3d5DGnegQuXenI29JDaMGoMvioKRBN53d4UazakS2unu8BnsEtxutS2kqRhYBPYk9RAriU3gaMgMB4wDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCB4AwCgYIKoZIzj0EAwIDSAAwRQIhAI5wBBAA3ewqIwslhuzFn4rNFW9dkz2TY7xeImO7CraYAiAYhai1NzJ6abAiYg8HxcRdYpO4bu2Sej8E6CzFHK34Yw==\n-----END CERTIFICATE-----"
],
"trustedRootCAs": [
"-----BEGIN CERTIFICATE-----\nMIIBQzCB66ADAgECAgjbHnT+6LsrbDAKBggqhkjOPQQDAjAYMRYwFAYDVQQDDA1NRE9DIFJPT1QgQ1NQMB4XDTI0MDUwMjEzMTMzMFoXDTI0MDUwMzEzMTMzMFowFzEVMBMGA1UEAwwMTURPQyBST09UIENBMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWP0sG+CkjItZ9KfM3sLF+rLGb8HYCfnlsIH/NWJjiXkTx57ryDLYfTU6QXYukVKHSq6MEebvQPqTJT1blZ/xeKMgMB4wDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCAQYwCgYIKoZIzj0EAwIDRwAwRAIgWM+JtnhdqbTzFD1S3byTvle0n/6EVALbkKCbdYGLn8cCICOoSETqwk1oPnJEEPjUbdR4txiNqkHQih8HKAQoe8t5\n-----END CERTIFICATE-----\n"
]
}
```
**Body Parameters**
- `issuerKey`: _JSON_ - A JWK or reference object to a key stored in an external KMS to sign the credential with.
Supported algorithms: ed25519, secp256k1, secp256r1, or
RSA.
**JWK Format:** `{"type": "jwk", "jwk": Here JSON Web Key Object}`. Can be provided as String Or JWK object to "
issuerKey".
**KMS Key**: Please refer to the [Key Management Section](https://docs.walt.id/community-stack/issuer/key-management/overview.md) and the KMS you want to
use for more details on the structure
of the reference object.
- `credentialConfigurationId`: _String_ - Reference to a specific credential configuration the issuer supports. As our
issuer currently supports W3C JWT & SD-JWT credentials and SD-JWT VCs (IETF). The structure of the credentialConfigurationId is the following: "
credentialType_jwt_vc_json" for W3C JWT & SD-JWT credentials
and "credentialType_vc+sd-jwt" for SD-JWT VCs (IETF). E.g. "UniversityDegree_jwt_vc_json" or "UniversityDegree_vc+sd-jwt".
You can also view
the credentialConfigurationIds by visiting the `/.well-known/openid-credential-issuer` endpoint of your issuer.
The metadata of our deployed issuer for testing can be seen [here](https://issuer.demo.walt.id/swagger/index.html#/oidc/get__standardVersion___well_known_openid_credential_issuer).
- `credentialData`: _JSON_ - Credential data structure to sign.
- `mapping` (optional): _JSON_ - The mapping object that allows for **dynamic value insertion via data functions**,
executed at the time when the credentials is claimed. This feature enables personalized credentials based on real-time
data. Learn more about it and see a list of supported data functions [here](https://docs.walt.id/community-stack/issuer/data-functions).
- `authenticationMethod`: (optional) _String_ - Defines which OIDC4VC exchange flow is used (pre-auth or full-auth). If
no value is
provided, it will default to pre-auth flow indicated as `PRE_AUTHORIZED`. The parameter options are:
Expand To Learn More About The Options
- `PWD` - used for authorization code flow with username/password authentication with external auth server
- `ID_TOKEN` - used for authorization code flow with id_token authentication. When this method is used an authorization
request is sent to the wallet, which generates and issues the ID_TOKEN using its DID (
Decentralized Identifier). The issuer then verifies the ID_TOKEN to confirm the holder's identity and DID.
- `VP_TOKEN` - used for authorization code flow with vp_token(OIDC4VP). With this method, the receiver of the
credential must be authenticated by presenting the requested credential. The credential which must be presented can
be configured using the `vpRequestedValue` as explained below. The policies which are applied to the presented credential
are 'signature,' 'expired,' and 'not-before.' Learn more about policies [here](https://docs.walt.id/community-stack/verifier/credential-verification/policies/overview.md).
- `NONE` - used for authorization code flow with none authentication method
- `PRE_AUTHORIZED` - used for pre-authorizated code flow
**Additional parameters required for selected auth method:**
**For "ID_TOKEN" and "VP_TOKEN":**
- `useJar`: (Optional) _Boolean_ - used for using JAR OAuth specification in the id/vp_token requests. If omitted, the
default value is true.
**For VP_TOKEN:**
- `vpRequestedValue`: _String_ - Specifies the requested credential type value for the VP token. E.g. "VerifiableId"
- `vpProfile`: (Optional) _String_ - Specifies the profile of the VP request. Available Profiles: DEFAULT: For W3C
OpenID4VP,
ISO_18013_7_MDOC: For MDOC OpenID4VP, EBSIV3: For EBSI V3 Compliant VP. If omitted, the default value is DEFAULT
- [`selectiveDisclosure`](https://docs.walt.id/concepts/selective-disclosure/intro) (optional): _JSON_ -An object that configures which
claims in the credential should be
selectively disclosable. It's manged through the following properties:
Expand To Learn More
- `fields`: An object illustrating the hierarchical structure of the credential contents. Each key in this object
specifies the name of the fields in the credential. Every field, whether high-level or nested, can be represented
by an object with a "sd" key. If the value for "sd" is set to true, it means that the corresponding field is
selectively disclosable. Moreover, fields that have nested attributes are represented with a "children" key which
contains another fields object reflecting the structure of the nested object. For example:
```json
{
"fields": {
"issuanceDate": {
"sd": true
},
"credentialSubject": {
"sd": false,
"children": {
"fields": {
"degree": {
"sd": false,
"children": {
"fields": {
"name": {
"sd": true
}
}
}
}
}
}
}
}
}
```
- `x5Chain` - contains the certificate chain used to verify the issuer's digital signature. The last certificate in the
chain must contain the public key of the private key which signs the to be issued credential.
- `trustedRootCAs` - contains trusted root certificates to validate the authenticity of the issuer's certificate chain.
---
**Example Response**
The issuer endpoint will respond with Credential Offer URL.
**Plain Response**
```
openid-credential-offer://issuer.potential.walt-test.cloud/?credential_offer_uri=https%3A%2F%2Fissuer.potential.walt-test.cloud%2Fopenid4vc%2FcredentialOffer%3Fid%3D9aabdb65-defe-464b-baa0-9cc13b36074a
```
**Decoded**
```
{
"credential_issuer": "https://issuer.potential.walt-test.cloud",
"credential_configuration_ids": [
"identity_credential_vc+sd-jwt"
],
"grants": {
"authorization_code": {
"issuer_state": "9aabdb65-defe-464b-baa0-9cc13b36074a"
},
"urn:ietf:params:oauth:grant-type:pre-authorized_code": {
"pre-authorized_code": "eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiI5YWFiZGI2NS1kZWZlLTQ2NGItYmFhMC05Y2MxM2IzNjA3NGEiLCJpc3MiOiJodHRwczovL2lzc3Vlci5wb3RlbnRpYWwud2FsdC10ZXN0LmNsb3VkIiwiYXVkIjoiVE9LRU4ifQ.vl9fBjoYUqr1nrA0jZ3ZjpS45yHLp2roMPCxCoqjuLNyBhTGO0g_PXMw8_NWhD-3qllRq5J0kLAw8WmfvT95Cw"
}
}
}
```
### Step 3: Receive the Credential Offer
The created credential offer can be used in two ways:
- **Scan via QR code** – embed the credential offer URL into a QR code for users to scan with their mobile wallet.
- **Paste manually** – copy the credential offer URL and paste it into the credential offer field of our [web wallet](https://wallet.demo.walt.id/).
To try it out with our web wallet:
1. Open the [web wallet](https://wallet.demo.walt.id/).
2. Log in.
3. Click the **request credential** button.
4. Paste the received Offer URL into the text field below the camera.
🎉 Congratulations, you've issued a SD-JWT VC Credential using OID4VCI! 🎉
To learn how to accept this credential offer programmatically using the Wallet API, see [How to Accept SD-JWT VC Credentials via OID4VCI](https://docs.walt.id/community-stack/wallet/credential-exchange/guides/accept-sd-jwt-vc-oid4vci.md).
--------------------------------------------------------------------------------
PAGE: How to issue Mobile Driver’s Licenses (ISO/IEC 18013-5 mDL) via OID4VCI with walt.id
URL: https://docs.walt.id/community-stack/issuer/credential-issuance/mdl-oid4vc
RAW: https://docs.walt.id/community-stack/issuer/credential-issuance/mdl-oid4vc.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# How to Issue Mobile Driver’s Licenses (ISO/IEC 18013-5 mDLs) via OID4VCI with walt.id
**TL;DR**
**What you’ll learn:**
- Generate IACA root certificates and keys that anchor trust for mDL issuance.
- Provision document signer certificates with EKU and CRL metadata required by ISO/IEC 18013-5.
- Populate mandatory mDL claims and invoke the walt.id Issuer API’s OID4VCI endpoint to sign the mdoc.
- Construct and distribute credential offer URLs so compliant wallets can request the issued credential.
**Relevant concepts:**
- [Mobile Driver's License (mDL)](https://docs.walt.id/concepts/digital-credentials/mdoc-mdl-iso.md) – ISO/IEC 18013-5/7 standard for mobile driver's licenses.
- [OID4VCI](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md) – protocol for issuing verifiable credentials to wallets.
Video: https://youtu.be/aRoBfjJvqEI
This guide provides a comprehensive walkthrough for issuing an mDL credential based on
the [ISO/IEC 18103-5 standard](https://www.iso.org/standard/69084.html) using the walt.id Issuer API.
The issuance process will utilize the
[OID4VCI protocol](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-ID1.html).
## Setup
See how to access to the issuer API below.
- [Deployed (Testing Only)](https://issuer.demo.walt.id/swagger/index.html) - Use our deployed version for testing.
- [Local](https://docs.walt.id/community-stack/issuer/setup.md) - Run the API in your environment with our open-source setup.
## Preparing for Issuance: Key Components
The mobile driver's license (mDL) ecosystem builds on established PKI principles that employ X.509 digital
certificates, while introducing domain-specific roles and data models. Its two main components are:
1. **IACA — Issuing Authority Certification Authority**: This is an authority that serves as the root of trust
vouching for authorized credential issuers and is represented by a self-signed X.509 digital certificate that is
published to verifiers.
2. **DS — Document Signer (Credential Issuer)**: This is the entity that issues and cryptographically signs mDL
credential(s). It holds a private key and an X.509 digital certificate issued by an IACA.
Typically, DS certificates are short-lived and are rotated periodically.
3. **The mDL Claims**: The mDL is a verifiable credential that contains claims about the subject (holder), such as
name, birthdate and driving privileges. The [ISO/IEC 18103-5 standard document](https://www.iso.org/standard/69084.html)
defines a set of mandatory fields (e.g., `family_name`, `birth_date`) that must be included in every mDL, as well
as a set of optional fields (e.g., `age_over_NN` attestations, `height`, `weight`, `sex`).
### Example mDL Credential in JSON format:
```json
{
"family_name": "Doe",
"given_name": "John",
"birth_date": "1986-03-22",
"issue_date": "2019-10-20",
"expiry_date": "2024-10-20",
"issuing_country": "AT",
"issuing_authority": "AT DMV",
"document_number": "123456789",
"portrait": [ 141, 12 ],
"driving_privileges": [
{
"vehicle_category_code": "A",
"issue_date": "2018-08-09",
"expiry_date": "2024-10-20"
},
{
"vehicle_category_code": "B",
"issue_date": "2017-02-23",
"expiry_date": "2024-10-20"
}
],
"un_distinguishing_sign": "AT"
}
```
**Note**: mDLs are encoded in CBOR, which is a binary format and is shown in JSON representation above for readability
purposes. Claims values that are encoded as JSON integer arrays, e.g., the `portrait` claim's value, represent byte
arrays.
## Issuing an mDL
In this section, we'll walk through the steps required to successfully issue an mDL by generating signing keys (for
the IACA & the DS) which we store ourselves. For production
environments, we recommend using an external KMS provider for key management due to the enhanced security.
Learn more about the different types of keys and the storage options [here](https://docs.walt.id/community-stack/issuer/key-management/overview.md).
**Note**: At the moment, you can only use `secp256r1` keys to onboard IACAs and DSs and to issue mDLs.
### Step 1: Onboard an Issuing Authority Certification Authority (IACA)
Every mobile driver’s license ecosystem starts with a trusted root certificate of an IACA. The IACA serves as the
cryptographic authority that issues DS certificates that are deemed trustworthy.
**:wrench: What this step does**
- Create a self-signed X.509 root certificate (CA=true).
- Encode identity info (e.g., country, organization) for the IACA in the certificate.
- Include optional extensions (e.g., IACA alternative names, CRL distribution URIs).
#### IACA Onboarding Request
**Option: CURL**
**Endpoint:** `/onboard/iso-mdl/iacas` | [API Reference](https://issuer.demo.walt.id/swagger/index.html#/Onboarding%20Service/post_onboard_iso_mdl_iacas)
**Example Request**
```bash
curl -X 'POST' \
'http://0.0.0.0:7002/onboard/iso-mdl/iacas' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"certificateData": {
"country": "US",
"commonName": "Example IACA",
"issuerAlternativeNameConf": {
"uri": "https://iaca.example.com"
}
}
}'
```
**Body**
```json
{
"certificateData": {
"country": "US",
"commonName": "Example IACA",
"issuerAlternativeNameConf": {
"uri": "https://iaca.example.com"
}
}
}
```
**Body Parameters**
- `certificateData`: _JSON_ - Data that will be encoded in the generated certificate of the IACA.
- `country`: _String_ - Two-letter ISO 3166-1 alpha-2 country code (e.g., `US`, `DE`).
- `commonName`: _String_ - Human-readable name of the issuing authority (e.g., `Ministry of Transport`).
- `issuerAlternativeNameConf`: _JSON_ - Metadata for the IACA; can include uri and/or email.
- `uri`: _String_ - Uniform resource identifier regarding the IACA's contact information.
- `email`: _String_ - RFC822 name for contacting the IACA.
---
**Example Response**
The IACA onboarding endpoint returns an object with:
- **`iacaKey`** – generated signing key of the IACA in JWK format.
- **`certificatePEM`** – PEM-encoded, JSON-stringified self-signed X.509 certificate of the IACA.
- **`certificateData`** – object containing the data that is encoded in the generated certificate (for convenience – useful for the DS onboarding endpoint below).
```json
{
"iacaKey": {
"type": "jwk",
"jwk": {
"kty": "EC",
"d": "u-UvsghdzpSXv5HmG5ngvm4Dv8yyRYw9fKA6mdp1KWs",
"crv": "P-256",
"kid": "R_E_QZ-Ea6etoAdWfUHSjjexRYz447ffnnfIO9kxn_Y",
"x": "n_b1GmZTSEhioK3z8MGqcb7nxXqyjFaLR-OfKOnspwU",
"y": "nGRVvuHTtEAZ1HjgdLaLZnYxrkiRV_e4V2Wz0qVWa-M"
}
},
"certificatePEM": "-----BEGIN CERTIFICATE-----\nMIIBtTCCAVqgAwIBAgIUNlgkpoam39UxORhMNRkwuFzD9pQwCgYIKoZIzj0EAwIwJDELMAkGA1UEBhMCVVMxFTATBgNVBAMMDEV4YW1wbGUgSUFDQTAeFw0yNTA1MjgxMjIzMDFaFw00MDA1MjQxMjIzMDFaMCQxCzAJBgNVBAYTAlVTMRUwEwYDVQQDDAxFeGFtcGxlIElBQ0EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASf9vUaZlNISGKgrfPwwapxvufFerKMVotH458o6eynBZxkVb7h07RAGdR44HS2i2Z2Ma5IkVf3uFdls9KlVmvjo2owaDAdBgNVHQ4EFgQUjCMRsfolTeK5Ds6MqOWj5Nx01BQwEgYDVR0TAQH/BAgwBgEB/wIBADAjBgNVHRIEHDAahhhodHRwczovL2lhY2EuZXhhbXBsZS5jb20wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMCA0kAMEYCIQCnUfp3OyxcaPCT34SQ4dTNyNN0qgxKWpWIDeUXkrs7HwIhALFYrMrINeAats4ZWRxZMK6bykb9dcOwkmBCv96MoZVi\n-----END CERTIFICATE-----\n",
"certificateData": {
"country": "US",
"commonName": "Example IACA",
"issuerAlternativeNameConf": {
"uri": "https://iaca.example.com"
}
}
}
```
**Defaults**:
- A local (`jwk`) secp256r1 key is automatically created.
- Validity defaults to 15 years, unless otherwise specified.
**Note:**
As we've used the local (`jwk`) key type, it's important to note that we need to save the returned values ourselves
for future reference. The API doesn't save any information about created keys.
**Note:**
Remember to save the value of the IACA's PEM-encoded X.509 certificate (`certificatePEM` field in the response) as
it is required for verification of
[mDLs](../../../98.verifier/1.api/3.credential-verification/4.mdl-oid4vc.md#example-verification-request).
### Step 2: Onboard a Document Signer
Document signers are the entities responsible for signing mDL credentials. These entities are identified by an X.509
digital certificate that is signed by an IACA and contains, among others, a special purpose extension that marks it
as valid for mDL issuance.
**:wrench: What this step does**
- Issue a DS certificate signed by the input IACA private key.
- Embed an Extended Key Usage (EKU) value marking the DS certificate as suitable for mDL signing.
- Include CRL distribution info and various other optional fields.
#### DS Onboarding Request
**Option: CURL**
**Endpoint:** `/onboard/iso-mdl/document-signers` | [API Reference](https://issuer.demo.walt.id/swagger/index.html#/Onboarding%20Service/post_onboard_iso_mdl_document_signers)
**Example Request**
```bash
curl -X 'POST' \
'http://0.0.0.0:7002/onboard/iso-mdl/document-signers' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"iacaSigner": {
"iacaKey": {
"type": "jwk",
"jwk": {
"kty": "EC",
"crv": "P-256",
"kid": "R_E_QZ-Ea6etoAdWfUHSjjexRYz447ffnnfIO9kxn_Y",
"x": "n_b1GmZTSEhioK3z8MGqcb7nxXqyjFaLR-OfKOnspwU",
"y": "nGRVvuHTtEAZ1HjgdLaLZnYxrkiRV_e4V2Wz0qVWa-M",
"d": "u-UvsghdzpSXv5HmG5ngvm4Dv8yyRYw9fKA6mdp1KWs"
}
},
"certificateData": {
"country": "US",
"commonName": "Example IACA",
"notBefore": "2025-05-28T12:23:01Z",
"notAfter": "2040-05-24T12:23:01Z",
"issuerAlternativeNameConf": {
"uri": "https://iaca.example.com"
}
}
},
"certificateData": {
"country": "US",
"commonName": "Example DS",
"crlDistributionPointUri": "https://iaca.example.com/crl"
}
}'
```
**Body**
```json
{
"iacaSigner": {
"iacaKey": {
"type": "jwk",
"jwk": {
"kty": "EC",
"crv": "P-256",
"kid": "R_E_QZ-Ea6etoAdWfUHSjjexRYz447ffnnfIO9kxn_Y",
"x": "n_b1GmZTSEhioK3z8MGqcb7nxXqyjFaLR-OfKOnspwU",
"y": "nGRVvuHTtEAZ1HjgdLaLZnYxrkiRV_e4V2Wz0qVWa-M",
"d": "u-UvsghdzpSXv5HmG5ngvm4Dv8yyRYw9fKA6mdp1KWs"
}
},
"certificateData": {
"country": "US",
"commonName": "Example IACA",
"notBefore": "2025-05-28T12:23:01Z",
"notAfter": "2040-05-24T12:23:01Z",
"issuerAlternativeNameConf": {
"uri": "https://iaca.example.com"
}
}
},
"certificateData": {
"country": "US",
"commonName": "Example DS",
"crlDistributionPointUri": "https://iaca.example.com/crl"
}
}
```
**Body Parameters**
- `iacaSigner`: _JSON_ - Object containing necessary data related to the signing IACA.
- `iacaKey`: _JSON_ - The JSON serialized signing key of the issuing IACA - used as is from the its respective
onboarding endpoint.
- `certificateData`: _JSON_ - The data that is encoded in the IACA's X.509 certificate - used as is from its
respective onboarding endpoint.
- `notBefore`: _String_ - ISO 8601 timestamp defining the start of the certificate's validity period (e.g. `2025-05-28T12:23:01Z`). Required.
- `notAfter`: _String_ - ISO 8601 timestamp defining the end of the certificate's validity period (e.g. `2040-05-24T12:23:01Z`). Required.
- `certificateData`: _JSON_ - Data that will be encoded in the generated certificate of the DS.
- `country`: _String_ - Two-letter ISO 3166-1 alpha-2 country code (e.g., `US`, `DE`).
- `commonName`: _String_ - Human-readable name of the issuing authority (e.g., `Ministry of Transport`).
- `crlDistributionPointUri`: _String_ - URL where the relevant certificate revocation list (CRL) is published.
**Requirements**
- Must provide the IACA's signing key and certificate data (as obtained from the IACA onboarding endpoint).
- Countries and state/province fields must match between IACA and DS.
- CRL URI is *mandatory* for DS onboarding.
---
**Example Response**
The DS onboarding endpoint returns an object with:
- **`documentSignerKey`** – generated signing key of the DS in JWK format.
- **`certificatePEM`** – PEM-encoded, JSON-stringified X.509 certificate of the DS signed by the IACA signing key in the request.
- **`certificateData`** – object that contains the data encoded in the generated certificate of the DS (for convenience).
```json
{
"documentSignerKey": {
"type": "jwk",
"jwk": {
"kty": "EC",
"d": "ZSHgIcRvbwV9s224kHUaFqkEPShCAdwXocGl_w3M42Q",
"crv": "P-256",
"kid": "pX99OZjL2iNqM7OMkE1r1rYyuAObvPntewcDHdc2bMM",
"x": "GWKpdL3jPoPJ5wKgSA-jxS2jgp-ZUDE6sIQbeB86vF0",
"y": "F3xAwH96_xVciV7mFQslU_eRQgP-5pSZiNf8bjMoGfo"
}
},
"certificatePEM": "-----BEGIN CERTIFICATE-----\nMIICCDCCAa2gAwIBAgIUDo8kr194t6sttt6KL3YcnMtcaYYwCgYIKoZIzj0EAwIwJDELMAkGA1UEBhMCVVMxFTATBgNVBAMMDEV4YW1wbGUgSUFDQTAeFw0yNTA1MjkwNzE4MzlaFw0yNjA4MjkwNzE4MzlaMCIxCzAJBgNVBAYTAlVTMRMwEQYDVQQDDApFeGFtcGxlIERTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEGWKpdL3jPoPJ5wKgSA+jxS2jgp+ZUDE6sIQbeB86vF0XfEDAf3r/FVyJXuYVCyVT95FCA/7mlJmI1/xuMygZ+qOBvjCBuzAfBgNVHSMEGDAWgBSMIxGx+iVN4rkOzoyo5aPk3HTUFDAdBgNVHQ4EFgQU7S49LSeg/e0onfT44FVbL/rSKnswDgYDVR0PAQH/BAQDAgeAMCMGA1UdEgQcMBqGGGh0dHBzOi8vaWFjYS5leGFtcGxlLmNvbTAVBgNVHSUBAf8ECzAJBgcogYxdBQECMC0GA1UdHwQmMCQwIqAgoB6GHGh0dHBzOi8vaWFjYS5leGFtcGxlLmNvbS9jcmwwCgYIKoZIzj0EAwIDSQAwRgIhAMuSq75BPBXXBWGtIMd57fhRqpKf3Yzl3ldDdoQsK2xEAiEA/dmWLMLiJPV3UzmQS5MUHtn611z0VlL/k3YAdaVJ51c=\n-----END CERTIFICATE-----\n",
"certificateData": {
"country": "US",
"commonName": "Example DS",
"crlDistributionPointUri": "https://iaca.example.com/crl"
}
}
```
**Defaults**:
- A local (`jwk`) secp256r1 key is automatically created.
- Validity defaults to 457 days, unless otherwise specified.
**Note:**
As we've used the local (`jwk`) key type, it's important to note that we need to save the returned values ourselves
for future reference. The API doesn't save any information about created keys.
**Note:**
Remember to save the value of the DS PEM-encoded X.509 certificate (`certificatePEM` field in the response) as
it is required for the next (issuance) step.
### Step 3: Issue the mDL
To issue mDLs:
1. Use the document signer's key (obtained previously).
2. Utilize the OID4VCI protocol to facilitate the issuance of the mDL from the document signer (issuer) to the holder.
3. Generate a credential offer URL that can be accepted by any compliant wallet to receive the credential.
**Note:**
The credential offer URL specifies the credentials to be issued. This includes details such as
the URL of the issuer and information about the credential's format and type.
#### mDL Issuance Request
**Option: CURL**
**Endpoint:**
`/openid4vc/mdoc/issue` | [API Reference](https://issuer.portal.test.waltid.cloud/swagger/index.html#/Credential%20Issuance/post_openid4vc_mdoc_issue)
**Example Request**
```bash
curl -X 'POST' \
'http://0.0.0.0:7002/openid4vc/mdoc/issue' \
-H 'accept: */*' \
-H 'statusCallbackUri: https://example.com/$id' \
-H 'Content-Type: application/json' \
-d '{
"issuerKey": {
"type": "jwk",
"jwk": {
"kty": "EC",
"d": "ZSHgIcRvbwV9s224kHUaFqkEPShCAdwXocGl_w3M42Q",
"crv": "P-256",
"kid": "pX99OZjL2iNqM7OMkE1r1rYyuAObvPntewcDHdc2bMM",
"x": "GWKpdL3jPoPJ5wKgSA-jxS2jgp-ZUDE6sIQbeB86vF0",
"y": "F3xAwH96_xVciV7mFQslU_eRQgP-5pSZiNf8bjMoGfo"
}
},
"credentialConfigurationId": "org.iso.18013.5.1.mDL",
"mdocData": {
"org.iso.18013.5.1": {
"family_name": "Doe",
"given_name": "John",
"birth_date": "1986-03-22",
"issue_date": "2019-10-20",
"expiry_date": "2024-10-20",
"issuing_country": "AT",
"issuing_authority": "AT DMV",
"document_number": "123456789",
"portrait": [ 141, 182 ],
"driving_privileges": [
{
"vehicle_category_code": "A",
"issue_date": "2018-08-09",
"expiry_date": "2024-10-20"
},
{
"vehicle_category_code": "B",
"issue_date": "2017-02-23",
"expiry_date": "2024-10-20"
}
],
"un_distinguishing_sign": "AT"
}
},
"x5Chain": [
"-----BEGIN CERTIFICATE-----\nMIICCDCCAa2gAwIBAgIUDo8kr194t6sttt6KL3YcnMtcaYYwCgYIKoZIzj0EAwIwJDELMAkGA1UEBhMCVVMxFTATBgNVBAMMDEV4YW1wbGUgSUFDQTAeFw0yNTA1MjkwNzE4MzlaFw0yNjA4MjkwNzE4MzlaMCIxCzAJBgNVBAYTAlVTMRMwEQYDVQQDDApFeGFtcGxlIERTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEGWKpdL3jPoPJ5wKgSA+jxS2jgp+ZUDE6sIQbeB86vF0XfEDAf3r/FVyJXuYVCyVT95FCA/7mlJmI1/xuMygZ+qOBvjCBuzAfBgNVHSMEGDAWgBSMIxGx+iVN4rkOzoyo5aPk3HTUFDAdBgNVHQ4EFgQU7S49LSeg/e0onfT44FVbL/rSKnswDgYDVR0PAQH/BAQDAgeAMCMGA1UdEgQcMBqGGGh0dHBzOi8vaWFjYS5leGFtcGxlLmNvbTAVBgNVHSUBAf8ECzAJBgcogYxdBQECMC0GA1UdHwQmMCQwIqAgoB6GHGh0dHBzOi8vaWFjYS5leGFtcGxlLmNvbS9jcmwwCgYIKoZIzj0EAwIDSQAwRgIhAMuSq75BPBXXBWGtIMd57fhRqpKf3Yzl3ldDdoQsK2xEAiEA/dmWLMLiJPV3UzmQS5MUHtn611z0VlL/k3YAdaVJ51c=\n-----END CERTIFICATE-----\n"
]
}'
```
**Header Parameters**
- `statusCallbackUri`: _URL_ - Receive updates on the created issuance process, e.g. when a credential was successfully
claimed. The parameter expects a URL which can accept a JSON POST request. The URL can also hold a `$id`, which will
be
replaced by the issuance session id. For example: `https://myurl.com/$id`, `https://myurl.com`
or `https://myurl.com/test/$id`
Expand To Learn More
**Body**
The data send to the provided URL will contain a JSON body:
- `id` : _String_ - the issuance session id
- `type`: _String_ - the event type
- `data`: _JsonObject_ - the data for the event
**Event Types**
Possible events (event types) and their data are:
- `resolved_credential_offer` with the credential offer as JSON (in our Web Wallet: called when the issuance offer is
entered into the wallet, but not processing / accepted yet)
- `requested_token` with the issuance request for the token as json object (called for the token required to receive the
credentials)
Credential issuance (called for every credential that's issued (= requested from wallet))
- `jwt_issue` with `jwt` being the issued jwt
- `sdjwt_issue` with `sdjwt` being the issued sdjwt
- `batch_jwt_issue` with `jwt` being the issued jwt
- `batch_sdjwt_issue` with `sdjwt` being the issued sdjwt
- `generated_mdoc` with `mdoc` being the CBOR (HEX) of the signed mdoc
To allow for secure business logic flows, if a callback URL is set, and it cannot be reached, the issuance will not
commence further (after that point). If no callback URL is set, the issuance logic does not change in any way.
**Body**
```json
{
"issuerKey": {
"type": "jwk",
"jwk": {
"kty": "EC",
"d": "ZSHgIcRvbwV9s224kHUaFqkEPShCAdwXocGl_w3M42Q",
"crv": "P-256",
"kid": "pX99OZjL2iNqM7OMkE1r1rYyuAObvPntewcDHdc2bMM",
"x": "GWKpdL3jPoPJ5wKgSA-jxS2jgp-ZUDE6sIQbeB86vF0",
"y": "F3xAwH96_xVciV7mFQslU_eRQgP-5pSZiNf8bjMoGfo"
}
},
"credentialConfigurationId": "org.iso.18013.5.1.mDL",
"mdocData": {
"org.iso.18013.5.1": {
"family_name": "Doe",
"given_name": "John",
"birth_date": "1986-03-22",
"issue_date": "2019-10-20",
"expiry_date": "2024-10-20",
"issuing_country": "AT",
"issuing_authority": "AT DMV",
"document_number": "123456789",
"portrait": [ 141, 182 ],
"driving_privileges": [
{
"vehicle_category_code": "A",
"issue_date": "2018-08-09",
"expiry_date": "2024-10-20"
},
{
"vehicle_category_code": "B",
"issue_date": "2017-02-23",
"expiry_date": "2024-10-20"
}
],
"un_distinguishing_sign": "AT"
}
},
"x5Chain": [
"-----BEGIN CERTIFICATE-----\nMIICCDCCAa2gAwIBAgIUDo8kr194t6sttt6KL3YcnMtcaYYwCgYIKoZIzj0EAwIwJDELMAkGA1UEBhMCVVMxFTATBgNVBAMMDEV4YW1wbGUgSUFDQTAeFw0yNTA1MjkwNzE4MzlaFw0yNjA4MjkwNzE4MzlaMCIxCzAJBgNVBAYTAlVTMRMwEQYDVQQDDApFeGFtcGxlIERTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEGWKpdL3jPoPJ5wKgSA+jxS2jgp+ZUDE6sIQbeB86vF0XfEDAf3r/FVyJXuYVCyVT95FCA/7mlJmI1/xuMygZ+qOBvjCBuzAfBgNVHSMEGDAWgBSMIxGx+iVN4rkOzoyo5aPk3HTUFDAdBgNVHQ4EFgQU7S49LSeg/e0onfT44FVbL/rSKnswDgYDVR0PAQH/BAQDAgeAMCMGA1UdEgQcMBqGGGh0dHBzOi8vaWFjYS5leGFtcGxlLmNvbTAVBgNVHSUBAf8ECzAJBgcogYxdBQECMC0GA1UdHwQmMCQwIqAgoB6GHGh0dHBzOi8vaWFjYS5leGFtcGxlLmNvbS9jcmwwCgYIKoZIzj0EAwIDSQAwRgIhAMuSq75BPBXXBWGtIMd57fhRqpKf3Yzl3ldDdoQsK2xEAiEA/dmWLMLiJPV3UzmQS5MUHtn611z0VlL/k3YAdaVJ51c=\n-----END CERTIFICATE-----\n"
]
}
```
**Body Parameters**
- `issuerKey`: _JSON_ - A JWK or reference object to a key stored in an external KMS to sign the mDL with.
Supported algorithms: secp256r1. This must be the **Document Signer (DS) key** obtained from the [DS onboarding step](#step-2-onboard-a-document-signer) (`documentSignerKey` field in the response). The private key here must correspond to the public key embedded in the DS certificate provided in `x5Chain`.
**JWK Format:** `{"type": "jwk", "jwk": Here JSON Web Key Object}`. Can be provided as String Or JWK object to "
issuerKey".
**KMS Key**: Please refer to the [Key Management Section](https://docs.walt.id/community-stack/issuer/key-management/overview.md) and the KMS you want to
use for more details on the structure
of the reference object.
- `credentialConfigurationId`: _String_ - org.iso.18013.5.1.mDL
- `mdocData`: _JSON_ - Claims to be added to the mDL. In the provided example above, all mandatory claims are included.
- `x5Chain`: _JSON Array_ - Must contain a single entry that contains the PEM-encoded, JSON stringified X.509
certificate of the DS (as output by the respective onboarding endpoint).
---
**Example Response**
The issuer endpoint will respond with Credential Offer URL.
**Plain Response**
```
openid-credential-offer://issuer.portal.test.waltid.cloud/draft13/?credential_offer_uri=https%3A%2F%2Fissuer.portal.test.waltid.cloud%2Fdraft13%2FcredentialOffer%3Fid%3D52b19ff5-5b42-423a-ad16-63099760baea
```
**Decoded**
```json
{
"credential_issuer": "https://issuer.portal.test.waltid.cloud/draft13",
"grants": {
"urn:ietf:params:oauth:grant-type:pre-authorized_code": {
"pre-authorized_code": "eyJ0eXBlIjoiand0IiwiYWxnIjoiRVMyNTYiLCJraWQiOiJ0R3pBbUthdE0tbzlicDM1Y291aDZUUXhxZHpVeFgzQWwtcU9iVXhZelh3In0.eyJzdWIiOiI1MmIxOWZmNS01YjQyLTQyM2EtYWQxNi02MzA5OTc2MGJhZWEiLCJpc3MiOiJodHRwczovL2lzc3Vlci5wb3J0YWwudGVzdC53YWx0aWQuY2xvdWQvZHJhZnQxMyIsImF1ZCI6IlRPS0VOIn0.aAgMH0XTq2YVTpsSGtwPIkinTai6Hr0iJyBttDOljPD2WV2kJolZR35K9hiqpz4ZBwGqUbHjmpYaywQqxb8Wfg"
}
},
"credential_configuration_ids": [
"org.iso.18013.5.1.mDL"
]
}
```
### Step 4: Receive the Credential Offer
The created credential offer can be used in two ways:
- **Scan as a QR code** – embed the offer into a QR code for users to scan with their mobile wallet.
- **Paste manually** – paste the offer into the credential offer field of our [web wallet](https://wallet.demo.walt.id/).
To try it out with our web wallet:
1. Open the [web wallet](https://wallet.demo.walt.id/).
2. Log in.
3. Click the **request credential** button.
4. Paste the received Offer URL into the text field below the camera.
🎉 Congratulations, you've issued a mDL using OID4VCI! 🎉
To learn how to accept this credential offer programmatically using the Wallet API, see [How to Accept a Mobile Driver's License via OID4VCI](https://docs.walt.id/community-stack/wallet/credential-exchange/guides/accept-iso-18013-5-mdl-oid4vci.md).
--------------------------------------------------------------------------------
PAGE: Data Functions
URL: https://docs.walt.id/community-stack/issuer/credential-issuance/data-functions
RAW: https://docs.walt.id/community-stack/issuer/credential-issuance/data-functions.md
STACK: Community Stack (open source) — version 0.23.0
--------------------------------------------------------------------------------
# Data Functions
The **walt.id issuer API** offers the use of **data functions** when issuing VCs. These functions ensure that the
credentials issued always contain the most current and accurate information.
**Note:**
Note: We now support data functions across all credential types: **W3C JWT VC**, **IETF SD-JWT VC** and **ISO mDoc** as part of our 0.20.0 release!
Video: https://youtu.be/4e-U_gkh09U
### How Data Functions Work
Data functions are not executed at the moment you create an OpenID credential offer URL via the walt.id issuer api.
Instead, they get executed at the point when the credential is actually claimed by the recipient. This means that the
data within the VC stays up-to-date, regardless of whether the recipient claims it immediately or after a
considerable period.
### The Importance Of Timing
This delayed execution is crucial because it guarantees the information's relevance and accuracy. For example,
a credential might be issued today but claimed a month later. The data functions ensure that the VC reflects
any changes that occurred during that interval, maintaining the credibility of the credential.
### How To Use Data Functions
When issuing a credential you have the option to provide a JSON Object called mapping that defines which keys in
the credential data structure should be replaced by the value returned of the provided data function. Data functions can
return any type of value from a String, Number of whole JSON Object. Therefore, as seen in the example below the degree
which is a nested object can be generated on the fly via the JSON webhook.
#### Example
**Credential To Issue**
```json
{
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://www.w3.org/2018/credentials/examples/v1"
],
"id": "",
"type": [
"VerifiableCredential",
"UniversityDegreeCredential"
],
"issuer": {
"id": ""
},
"credentialSubject": {
"degree": ""
}
}
```
**Mapping Object With Data Functions**
```json
{
"id": "",
"issuer": {
"id": ""
},
"degree": ""
}
```
**Example Issuance API Call Body**
```json
{
...
"credentialData": {
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://www.w3.org/2018/credentials/examples/v1"
],
"id": "",
"type": [
"VerifiableCredential",
"UniversityDegreeCredential"
],
"issuer": {
"id": ""
},
"credentialSubject": {
"degree": ""
}
},
"mapping": {
"id": "",
"issuer": {
"id": ""
},
"degree": ""
}
...
}
```
**Issued credential**
```json
{
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://www.w3.org/2018/credentials/examples/v1"
],
"id": "http://example.gov/credentials/3732",
// return value from data function
"type": [
"VerifiableCredential",
"UniversityDegreeCredential"
],
"issuer": {
"id": "did:web:vc.transmute.world"
// return value from data function
},
"credentialSubject": {
"degree": {
// return value from data function
"type": "BachelorDegree",
"name": "Bachelor of Science and Arts"
}
}
}
```
## Available Functions
These functions can be used within the mapping object of the issuance request. For details on issuance, you can
read [this guide](https://docs.walt.id/community-stack/issuer/credential-issuance/vc-oid4vc.md)
A list of functions with their correct syntax to be used in the mapping object below:
#### UUID
`` to get a unique identifier.
#### Subject Did
`` to get the recipient's DID once the credential gets claimed.
#### Issuer DID
`` to get the issuer's DID.
**Note:**
**Note:** This function is only available for **W3C JWT VC** issuance. It is not supported for IETF SD-JWT VC credentials.
#### Timestamp
`` to get the current timestamp in ISO 8601 format `YYYY-MM-DDThh:mm:ss.sssZ`
#### Timestamp Seconds
`` to get the Unix timestamp.
#### Timestamp In
`