# Welcome to Empeiria

Empeiria's **End-to-End Verifiable Data Infrastructure (EVDI)** is a complete platform for building applications with Self-Sovereign Identity. Whether you're a developer looking to integrate verifiable credentials, a validator wanting to secure the network, or a user exploring digital identity, this documentation will guide you through our ecosystem.

## What is EVDI?

EVDI provides microservices and SDKs to create, manage, and verify W3C-compliant Verifiable Credentials and Decentralized Identifiers — these services work off-chain, while the Cosmos-SDK EMPE blockchain is a separate component of the wider Empeiria ecosystem. Think of it as the infrastructure layer that lets you add cryptographically-verifiable data to any application without building your own identity stack or wrestling with evolving standards.

Our platform handles everything from issuing digital credentials (like certificates, licenses, or tickets) to verifying them in real-time.

## Quick Navigation

### For Developers

**New to the platform?** Start with our [live demo](/getting-started/quickstart) to experience the full flow using our mobile wallet — about five minutes, no setup.

**Ready to build?** Jump into our comprehensive [Tutorial](/getting-started/tutorial/overview) that walks you through creating your first issuer and verifier from scratch.

**Need rapid deployment?** Check out [One-Click Deployment](/develop/intro) to spin up complete SSI environments in minutes.

**Looking for technical details?** Explore our service documentation:

* [Issuer Service](/develop/issuer) - Issue and manage verifiable credentials
* [Verifier Service](/develop/verifier) - Verify credentials and manage access
* [Cloud Wallet (Wallet Server)](/develop/wallet-server) - Server-side holder wallet for automated flows, testing, and agents

**Want AI-powered SSI interactions?** The [MCP Server](https://github.com/empe-io/mcp-server), a companion project, lets AI assistants like Claude manage schemas, credential offers, and verification requests on your issuer and verifier deployments through MCP tools.

### For Validators

**Want to secure our network?** Head to the [Validators Guide](/empe-blockchain/overview) for setup instructions, hardware requirements, and operational guidance.

**Need technical support?** Check our [Validator FAQ](/empe-blockchain/overview/faq) or reach out to <validators@empe.io>.

### For Users

**Using our mobile wallet?** The [Empe DID Wallet Guide](/empe-blockchain/blockchain-intro/00_introduction) covers everything from setup to credential management.

**Managing blockchain assets?** Learn about [Keplr Wallet integration](/empe-blockchain/overview/keplr) for staking, governance, and token operations.

## Understanding the Technology

### Core Concepts

**New to Self-Sovereign Identity?** Our [SSI fundamentals section](/understand/self-sovereign-identity) explains the key concepts, technical foundations, and standards that power our platform.

**Want the technical details?** Dive into our [EVDI Architecture](/understand/intro) overview to understand how all the pieces fit together.

### Standards and Compliance

We build on established standards including W3C DIDs and VCs, OpenID4VCI, and OpenID4VP. Our platform ensures interoperability with other SSI systems while maintaining security and privacy.

## What You Can Build

Our infrastructure supports a wide range of use cases:

* **Digital credentials**: Educational certificates, professional licenses, event tickets
* **Supply chain tracking**: Product passports, authenticity verification, compliance records
* **Identity verification**: KYC/AML processes, age verification, membership proofs
* **Access control**: Secure building access, API authentication, content gating
* **AI agent interactions**: Verifiable data for autonomous systems and trusted AI workflows

## Getting Help

* **Technical questions**: Join our [Discord community](https://discord.com/invite/BbCzfuJzkv)
* **Business inquiries**: Contact us at <contact@empe.io>
* **Validator support**: <validators@empe.io>
* **Documentation feedback**: Help us improve these docs by reaching out

## What's Next?

The platform continues to evolve with upcoming features like zero-knowledge proofs and enhanced on-chain integrations. Check our roadmap and changelog for the latest developments.

Ready to start? Pick your path above and dive in. Whether you're prototyping your first credential or deploying production systems, we've designed this documentation to get you there efficiently.


# Introduction

Empeiria's **End-to-End Verifiable Data Infrastructure (EVDI)** is a complete platform for building applications with Self-Sovereign Identity. This section takes you from zero to issuing and verifying your first credential.

## What is EVDI?

EVDI is the infrastructure layer that lets you add cryptographically verifiable data to any application without building your own identity stack or wrestling with evolving standards. It provides standards-based services and REST APIs to:

* **Create decentralized identifiers** — generate `did:web` and `did:key` (Ed25519) DIDs, with DID documents served for you over HTTPS.
* **Define credential schemas** — describe the claims a credential carries and the formats it supports.
* **Issue Verifiable Credentials** over **OpenID4VCI**, in **SD-JWT VC** and **JWT-VC-JSON** formats.
* **Verify presentations** over **OpenID4VP**, using DCQL or DIF Presentation Exchange queries.

All of this runs as off-chain services, so you can issue and verify credentials in real time through a standards-based API.

This platform issues and verifies credentials off-chain with `did:web` and `did:key`. The `did:empe` method and EMPE blockchain are separate components of the wider Empeiria ecosystem — see [EVDI Architecture](/understand/intro).

From here, try the [live demo](/getting-started/quickstart) to see credentials in action, or follow the [Tutorial](/getting-started/tutorial) to build issuance and verification into your own app.


# Try the Live Demo

{% hint style="success" %}

### Run it yourself → [**demo.empe.io**](https://demo.empe.io/)

About five minutes. No account, no code, nothing to set up — the demo even installs the wallet app for you if you don't have it yet.
{% endhint %}

The fastest way to understand verifiable credentials is to hold one.

The demo leads you by the hand from start to finish. It plays both institutions for you: first the **issuer** that signs a document and hands it to you, then the **verifier** that asks you to prove it. You hold the wallet. A progress bar across the top — **Choose → Claim → Verify → Done** — tells you where you are, each screen tells you exactly what to tap, and the page reacts live as your phone scans.

At the end the verifier confirms your document is genuine without ever contacting the issuer: no database lookup, no phone call, no waiting. That is the whole idea, and five minutes of clicking explains it better than any page of documentation can.

***

## One Platform, Every Industry

The demo is not a single canned flow. It ships eleven scenarios drawn from different sectors, and every one of them runs on the same infrastructure — the same issuing, the same wallet, the same verification. Nothing is rebuilt per industry.

| Industry            | Credential            | Issued by                                  | Checked by                                 |
| ------------------- | --------------------- | ------------------------------------------ | ------------------------------------------ |
| Workplace access    | Employee Badge        | An employer                                | The same employer — at the office door     |
| Proof of employment | Employment Credential | An employer                                | A bank or a landlord — a different company |
| Know Your Customer  | Identity (KYC)        | A bank, after its KYC check                | Another service — an exchange or a telco   |
| Compliance          | AML Screening         | A compliance provider, after AML screening | A bank or a fintech, at onboarding         |
| Education           | University Diploma    | A university                               | An employer, during recruitment            |
| Government ID       | Driver's License      | A licensing authority                      | A car rental company                       |
| Access & entry      | Event Ticket          | The event organizer                        | The same organizer — at the gate           |
| Loyalty & tiers     | Membership Card       | A loyalty program                          | A store, at checkout                       |
| Health              | Vaccination Record    | A clinic                                   | An airline or a venue                      |
| Supply chain        | Product Passport      | The manufacturer                           | A distributor or customs                   |
| Civil registry      | Birth Certificate     | The civil registry office                  | Another office or a bank                   |

Two patterns run through that list, and both matter commercially:

* **One company on both sides.** The employer checks its own badge at the door; the organizer checks its own ticket at the gate. Credentials replace a proprietary access system, and the check keeps working with no connection to a backend.
* **Two different companies.** A bank relies on a compliance provider's AML screening; a recruiter accepts a university's diploma; a landlord accepts an employer's confirmation. The expensive work is done **once** and reused by everyone downstream, instead of every institution repeating the same check.

That second pattern is where the cost disappears: one KYC check, one AML screening, one diploma verification — issued once, accepted anywhere, with no integration built between the issuer and the verifier. On our platform a company can take either role, or both.


# Tutorial: Credential Issuance & Verification


# Overview

This tutorial guides you through building a working environment that issues and verifies Verifiable Credentials within the Empeiria ecosystem. You'll stand up a small backend that talks to the Issuer and Verifier APIs, create a credential schema, issue a credential to a wallet, and gate access to a protected page behind a successful verification.

The credential flows you'll implement are built on open standards: **OpenID4VCI** for issuance and **OpenID4VP** for verification, with credentials issued in **SD-JWT VC** format so the holder can selectively disclose individual claims.

**What You Will Learn:**

* How to stand up a hosted deployment with the One-Click Deployment platform, then create and configure an Issuer and Verifier against that deployment through its API and operator console.
* How to authenticate to the Issuer and Verifier APIs using an OIDC access token — an `Authorization: Bearer <JWT>` header (with the local-development shortcut of running the API with `AUTH_DISABLED=true`).
* How to run a simple Express-based backend that integrates with the Issuer and Verifier APIs.
* How to create a credential schema and issue credentials against it.
* How to implement verification logic so that only holders of a valid credential can access a protected resource.
* How to set up a simple frontend that displays QR codes for the issuance and verification flows.
* How to test the full flow using a reference mobile wallet application (iOS/Android).

**Prerequisites:**

* Access to the [One-Click Deployment platform](https://oneclick.empe.io/).
* Node.js and npm installed.
* Basic knowledge of JavaScript/TypeScript and Node.js.
* A reference Empe DID Wallet:
  * Android: <https://play.google.com/store/apps/details?id=io.empe.wallet>
  * iOS: <https://apps.apple.com/us/app/empe-wallet/id6468363115>

By the end of this tutorial, you'll have a demo that issues a "KYC Verifiable Credential" and verifies it to grant access to a protected dashboard.


# Understanding Key Concepts

Before we dive in, let's clarify a few core components of the Empeiria ecosystem:

* **Issuer**:\
  The Issuer is responsible for creating and issuing Verifiable Credentials. In this guide, the Issuer service is deployed for you via the One-Click Deployment portal. It stores credential schemas (which define the structure of credentials) and, upon request, creates credential offers that users can claim with their wallet.
* **Schema**:\
  A schema defines the fields and data types that a credential will include. By uploading a schema to the Issuer, you tell it the exact data structure of the credentials it will produce. For example, a "KYC Verifiable Credential" schema may include `age`, `first_name`, and `last_name`. Each schema is identified by a name and version, and combining it with a credential format produces a **credential configuration** (for example, `KYCCredential@1.0:sd-jwt`) that credential offers reference by id.
* **Verifier**:\
  The Verifier checks the authenticity and validity of credentials presented by a holder. It ensures that a credential was issued by a trusted Issuer and that it meets certain criteria—for example, that the user is over 18. The Verifier runs inside the same One-Click deployment as the Issuer; you register a verifier identity for it in the operator console. It creates authorization requests (presented to the user as a QR code or deep link) and tracks the outcome through a verification session.
* **Empe DID Wallet**:\
  A reference mobile wallet (available for iOS/Android) used to store and present credentials. The wallet holds its own decentralized identifier (such as a `did:key` or `did:web`). Users scan QR codes displayed by the Issuer or Verifier flows with this wallet. When issuing a credential, the user scans a code to add the credential to their wallet. When verifying, they scan a verification QR code to prove they hold a valid credential. Because credentials can be issued as SD-JWT, the wallet supports **selective disclosure**—the user can reveal only the specific claims a Verifier asks for, rather than the entire credential.

**Overall Flow:**

1. Create a One-Click deployment (it hosts both the Issuer and Verifier), then register a verifier identity in the console.
2. Upload a schema to the Issuer so it knows how to structure credentials.
3. Issue credentials by having the user submit their info, and display a QR code from the Issuer.
4. The user scans the issuance QR code with the wallet to claim the credential.
5. To verify, request a verification QR code from the Verifier and show it to the user.
6. The user scans the verification QR code with their wallet and presents the credential. When the check passes, the verification session moves to the `ResponseVerified` state. Your backend observes this by subscribing to the session's real-time event stream (`/verification-sessions/{sessionId}/events`) or by querying the session — and can then treat the user as verified.

In the following steps, we'll set everything up and run through these flows in detail.


# Project Setup

**What We Are Doing:**

* Initializing a Node.js/Express project.
* Installing a minimal set of dependencies.
* Preparing the base structure for talking to the Issuer and Verifier HTTP API.

**Why:** We need a backend server that:

* Calls the Issuer API to create credential offers.
* Calls the Verifier API to create authorization (presentation) requests and check results.
* Serves a small frontend for user input and QR code display.

The Issuer and Verifier are exposed as a plain HTTP API (issuance follows OpenID4VCI, verification follows OpenID4VP). Our app speaks to the API directly with the built-in `fetch`, sending a standard OIDC Bearer token on every request — no client library is needed. That keeps the dependency list small: an HTTP server, an env loader, and a JWT helper.

**Steps:**

1. Create a new directory and initialize a Node.js project:

```bash
    mkdir -p evdi-tutorial/{src,public} && \
    touch evdi-tutorial/src/{index.js,issuer-routes.js,verifier-routes.js,verification-flows.js,authorization-routes.js} \
      evdi-tutorial/public/{index.html,dashboard.html} \
      evdi-tutorial/.env
    cd evdi-tutorial
    npm init -y
    npm pkg set type=module
```

The `npm pkg set type=module` step marks the package as an ES module — all the code in this tutorial uses `import`/`export` syntax, which Node.js only accepts in `.js` files when `package.json` contains `"type": "module"`.

Project structure:

```
 evdi-tutorial/
   src/
     index.js                 # Express app entrypoint
     issuer-routes.js         # routes that call the Issuer API
     verifier-routes.js       # routes that call the Verifier API
     verification-flows.js    # the "kyc" verification flow + app-token helper
     authorization-routes.js  # session-token decode + protected dashboard routes
   public/
     index.html               # issuance page (shows the offer QR)
     dashboard.html           # protected page shown after verification
   .env
```

2. Install dependencies:

```bash
 npm install express dotenv jsonwebtoken
```

* **express** — runs our backend and serves the frontend pages.
* **dotenv** — loads the Issuer/Verifier URLs and token from `.env`.
* **jsonwebtoken** — signs and verifies our own short-lived app session token (used later to gate the protected dashboard).

We use the built-in `fetch` for HTTP calls (available in Node.js 18+), so no extra HTTP client is needed.

3. Add the API configuration to your `.env` file. The app authenticates to the Issuer and Verifier with an OIDC access token (an admin Bearer token from the deployment's Keycloak) sent as `Authorization: Bearer <token>`, so we store the base URLs and the per-service tokens here:

```
# Base URLs of the deployed Issuer and Verifier
ISSUER_BASE_URL=https://your-issuer.evdi.app
VERIFIER_BASE_URL=https://your-deployment.evdi.app

# Admin access tokens (Bearer/JWT) sent as: Authorization: Bearer <token>
ISSUER_ACCESS_TOKEN=your-oidc-access-token
VERIFIER_ACCESS_TOKEN=your-oidc-access-token

# Secret our app uses to sign its own short-lived session tokens
# (any long random string; used by the dashboard flow later in the tutorial)
JWT_SECRET=replace-with-a-long-random-string

# Port our tutorial app listens on
PORT=4000
```

These values are accessible in your code as `process.env.ISSUER_BASE_URL`, `process.env.VERIFIER_BASE_URL`, `process.env.ISSUER_ACCESS_TOKEN`, and `process.env.VERIFIER_ACCESS_TOKEN`. We will obtain the access tokens and fill in the base URLs in the next steps, when we create the Issuer and Verifier. Next, we will look at how authentication works — the OIDC Bearer token that every request in this tutorial carries.


# Authentication

**What We Are Doing:**

* Obtaining an OIDC access token — a signed JWT presented as `Authorization: Bearer <token>`.
* Making sure that token carries the `admin` role (in `realm_access.roles`) so it can reach every endpoint in this tutorial.
* Pointing the API at the identity provider that signs those tokens via `OIDC_JWKS_URL`.
* Using the `AUTH_DISABLED=true` shortcut for fast local development.

**Why:** Every protected endpoint on the Issuer and Verifier APIs is guarded by a standard OIDC JWT. Your client authenticates with exactly one credential: a valid OIDC Bearer token, sent in the `Authorization` header on every protected request. The service validates the token's signature against the identity provider's public keys, and for the agent/DID and wallet-management endpoints it additionally checks for the `admin` role. Once you have a token, every later step in this tutorial reuses the exact same header: `Authorization: Bearer <token>`.

## How authentication works

When a request arrives, the API:

1. Reads the JWT from the `Authorization: Bearer <token>` header.
2. Fetches the signing keys from the JWKS endpoint configured in `OIDC_JWKS_URL` and verifies the token's RS256 signature and expiry.
3. For agent/DID, issuer-record, verifier-record and holder/wallet management (the `/agent/*` and `/holders/:holderId/*` routes), checks that the token grants the `admin` role.

Two kinds of endpoint sit behind this:

* **Token + `admin` role** — the agent endpoints (`/agent/*`: DIDs, issuer records, verifier records, holders) and the cloud-wallet endpoints (`/holders/:holderId/*`). The `admin` role can come from the token's `realm_access.roles` array or from any client's `resource_access.<client>.roles` array; this tutorial uses `realm_access`.
* **Token only (no specific role)** — the Issuer endpoints (schemas, credential offers, issuance sessions, direct signing) and the Verifier endpoints (authorization requests, verification sessions, `/credentials/verify`). These require a valid Bearer token but do not check for a particular role.

A handful of endpoints are fully public and need no token at all: the root and `/version` endpoints, and the served DID documents (`/:uuid/did.json`, also served at `/:uuid/.well-known/did.json`).

To keep things simple, this tutorial uses one token that carries the `admin` role, so the same header works for every request.

## Where the token comes from

The access token is issued by an OIDC identity provider (for example, Keycloak) — the API never mints tokens itself, it only validates them. The provider exposes a **JWKS** (JSON Web Key Set) document containing the public keys used to verify token signatures, and the API is told where to find it through the `OIDC_JWKS_URL` environment variable:

```
# The JWKS endpoint of your identity provider (required unless AUTH_DISABLED=true)
OIDC_JWKS_URL=https://auth.example.com/realms/empe/protocol/openid-connect/certs
```

When you provision a deployment through the portal, this wiring is already done for you: you obtain a ready-to-use admin token from the deployment's Keycloak (see [Authenticating to Your Deployment](/develop/intro/authenticating-to-your-deployment)). For local development you run the identity provider yourself, or skip validation entirely with `AUTH_DISABLED=true` (see below).

## The token's shape

The token is an ordinary OIDC access token (a signed JWT). What the API looks at are the role claims — an `admin` role in `realm_access.roles` (or in any `resource_access.<client>.roles` array) is what unlocks the agent/DID and wallet routes. A decoded payload looks roughly like this:

```json
{
  "sub": "f81d4fae-7dec-11d0-a765-00a0c91e6bf6",
  "realm_access": {
    "roles": ["admin"]
  },
  "exp": 1750594800,
  "iss": "https://auth.example.com/realms/empe"
}
```

You never construct this yourself — the identity provider builds and signs it. You only need to make sure your provider assigns the `admin` role to the account whose token you use.

## Sending the token

Set the token (and your base URLs) as environment variables so the rest of the tutorial can reuse them:

```bash
ISSUER_BASE_URL=https://your-issuer.evdi.app
VERIFIER_BASE_URL=https://your-deployment.evdi.app
TOKEN=<your OIDC access token>   # a JWT carrying the admin role; stored per service as ISSUER_ACCESS_TOKEN / VERIFIER_ACCESS_TOKEN in the deploy pages
```

Then attach it to every protected request with the `Authorization` header:

```bash
curl "$ISSUER_BASE_URL/schemas" \
  -H "Authorization: Bearer $TOKEN"
```

A successful response confirms the token was accepted — on a fresh deployment, the schema list is simply empty:

```json
[]
```

The same `Authorization: Bearer $TOKEN` header applies to every administrative endpoint you'll use later — uploading and assigning schemas, creating credential offers (for example, referencing the `KYCCredential@1.0:sd-jwt` configuration), signing credentials directly, creating authorization requests, and managing holders.

If a token is missing, expired, or has an invalid signature, the request is rejected with `401`. If the token is valid but lacks the `admin` role on an agent or wallet route, the request is rejected with `403`. The error filter returns a consistent body:

```json
{
  "statusCode": 401,
  "message": "Unauthorized",
  "path": "/agent/dids",
  "timestamp": "2026-06-22T12:00:00.000Z"
}
```

## Local development: `AUTH_DISABLED=true`

While building and testing locally you usually don't want to stand up an identity provider just to call the API. Start the service with `AUTH_DISABLED=true` and every request is treated as an authenticated **admin**, so you can omit the `Authorization` header entirely:

```bash
AUTH_DISABLED=true npm run start:dev
```

With auth disabled, the same request needs no token:

```bash
curl "http://localhost:3000/schemas"
```

A few things to keep in mind:

* **`AUTH_DISABLED=true` is for local development only.** It disables all token validation and role checks, so never enable it on a deployed or shared environment.
* When `AUTH_DISABLED` is not set to `true`, `OIDC_JWKS_URL` is required — the service needs to know where to fetch the signing keys.
* Tokens must travel over HTTPS in any non-local environment so the credential is never sent in clear text. (`ALLOW_INSECURE_HTTP=true` exists for local HTTP only.)

## Trying it in the interactive API reference

Each deployment serves a live API reference at **`/api-docs`** (for example, `https://your-issuer.evdi.app/api-docs`) where you can browse every endpoint, paste your Bearer token once, and try requests directly from the browser.

<figure><img src="/files/jpo9cAHKGkCnyBvQCD3V" alt="Authorize button in the /api-docs Swagger UI"><figcaption><p>The <strong>Authorize</strong> button (top right) in the deployment's <code>/api-docs</code> Swagger UI</p></figcaption></figure>

With a valid admin token in hand (or `AUTH_DISABLED=true` running locally), you're ready to move on. Next, we'll create the DIDs our Issuer and Verifier are anchored to and register both identities.


# Creating DIDs and Registering Issuer & Verifier

**What We Are Doing:**

* Creating a decentralized identifier (DID) for the Issuer and one for the Verifier.
* Registering an **Issuer** bound to its DID so it can issue Verifiable Credentials.
* Registering a **Verifier** bound to its DID so it can request and check credential presentations.

**Why:** Every credential the Issuer signs and every authorization request the Verifier sends is anchored to a DID — a portable, cryptographically verifiable identity. A wallet that receives a credential resolves the Issuer's DID to fetch the public key that proves the credential is authentic; a wallet that responds to a presentation request resolves the Verifier's DID to confirm who is asking. Creating these identities is a one-time setup step that you complete before defining schemas or issuing anything.

The service creates two DID methods: **`did:key`** (a self-contained identifier where the public key is encoded directly in the identifier) and **`did:web`** (an identifier whose DID document the service hosts for you over HTTPS). It serves the `did:web` documents itself — you never register or anchor a DID on any external system.

Set up your environment first. The Agent/DID, Issuer, and Verifier registration endpoints live under `/agent` and require a valid OIDC Bearer token carrying the `admin` role. This tutorial keeps a separate base URL for the Issuer and the Verifier so each call clearly targets the right service. In the current One-Click setup both point at the same deployment (one service hosts the Issuer, Verifier, and Holder APIs), though a Verifier can run under its own host in other setups — so the two variables stay distinct:

```bash
ISSUER_BASE_URL=https://your-issuer.evdi.app       # your Issuer deployment
VERIFIER_BASE_URL=https://your-deployment.evdi.app # same deployment today; may be a separate host
TOKEN=<your OIDC access token>                     # JWT with the admin role
```

These are the same base URLs you prepared in your `.env` during [Project Setup](/getting-started/tutorial/project-setup). `TOKEN` here is the admin OIDC access token — the same value the deploy pages store per service as `ISSUER_ACCESS_TOKEN` / `VERIFIER_ACCESS_TOKEN`. The upcoming [Deploying the Issuer](/getting-started/tutorial/deploying-issuer) and [Deploying the Verifier](/getting-started/tutorial/deploying-verifier) pages show where each value comes from in the One-Click portal.

> **Local development:** with `AUTH_DISABLED=true` you can omit the `Authorization` header — see [Authenticating to the API](/getting-started/tutorial/authentication).

## Step 1 — Create a DID

You can create either a `did:key` or a `did:web`. A `did:key` requires no configuration and is ideal for getting started; a `did:web` produces a human-readable identifier tied to your domain whose document is resolvable at a stable HTTPS URL.

### Option A — Create a `did:key`

**`POST /agent/did/key`**

This request takes no body and returns the new DID:

```bash
curl -X POST "$ISSUER_BASE_URL/agent/did/key" \
  -H "Authorization: Bearer $TOKEN"
```

**Response Body**

```json
{
  "did": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
  "verificationMethodIds": [
    "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK#z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
  ]
}
```

* **did** — the new `did:key` identifier (an Ed25519 key encoded directly in the DID).
* **verificationMethodIds** — the verification method identifiers you can reference for signing and assertions.

### Option B — Create a `did:web`

**`POST /agent/did/web`**

**Request Body**

```json
{
  "domain": "issuer.example.com"
}
```

* **domain** *(required)* — the hostname that will host the DID document (for example, `issuer.example.com`, optionally with a port like `issuer.example.com:8080`). This must be a valid hostname, not a full URL.

```bash
curl -X POST "$ISSUER_BASE_URL/agent/did/web" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"domain":"issuer.example.com"}'
```

**Response Body**

```json
{
  "did": "did:web:issuer.example.com:0c8f3a52-9b2e-4d31-8a7c-1f6b9e4d2a10",
  "didDocument": {
    "@context": [
      "https://www.w3.org/ns/did/v1",
      "https://w3id.org/security/suites/ed25519-2018/v1"
    ],
    "id": "did:web:issuer.example.com:0c8f3a52-9b2e-4d31-8a7c-1f6b9e4d2a10",
    "verificationMethod": [
      {
        "id": "did:web:issuer.example.com:0c8f3a52-9b2e-4d31-8a7c-1f6b9e4d2a10#key-1",
        "type": "Ed25519VerificationKey2018",
        "controller": "did:web:issuer.example.com:0c8f3a52-9b2e-4d31-8a7c-1f6b9e4d2a10",
        "publicKeyBase58": "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV"
      }
    ],
    "authentication": [
      "did:web:issuer.example.com:0c8f3a52-9b2e-4d31-8a7c-1f6b9e4d2a10#key-1"
    ],
    "assertionMethod": [
      "did:web:issuer.example.com:0c8f3a52-9b2e-4d31-8a7c-1f6b9e4d2a10#key-1"
    ]
  }
}
```

* **did** — the created identifier, in the form `did:web:<domain>:<uuid>`. The service generates the trailing UUID for you.
* **didDocument** — the full DID document the service will host. It uses an Ed25519 verification key (`Ed25519VerificationKey2018`) whose public material is published as `publicKeyBase58`; the same key id is referenced from both `authentication` and `assertionMethod`.

The service hosts this `did:web` document for you. It is served, unauthenticated, at a URL derived from the DID's UUID:

```bash
curl "https://issuer.example.com/0c8f3a52-9b2e-4d31-8a7c-1f6b9e4d2a10/did.json"
```

The same document is also available at `https://issuer.example.com/0c8f3a52-9b2e-4d31-8a7c-1f6b9e4d2a10/.well-known/did.json`. When a wallet or another service resolves your `did:web`, this is the document it reads to obtain your public key — so no extra publishing step is required on your side.

**Save the returned `did`.** You will pass it when registering the Issuer (or Verifier) in the next steps, and it becomes the `:issuerDid` path segment used throughout the Issuer API.

## Step 2 — Register the Issuer

A DID on its own is just an identity. To start issuing credentials you register an **Issuer** bound to a DID you created in Step 1. The Issuer signs every credential with that DID's key and advertises the credentials it supports through its OpenID4VCI metadata.

**`POST /agent/issuer`**

**Request Body**

```json
{
  "did": "did:web:issuer.example.com:0c8f3a52-9b2e-4d31-8a7c-1f6b9e4d2a10"
}
```

* **did** *(required)* — the DID the Issuer is bound to. It must be a DID this service already created (from Step 1).

```bash
curl -X POST "$ISSUER_BASE_URL/agent/issuer" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"did":"did:web:issuer.example.com:0c8f3a52-9b2e-4d31-8a7c-1f6b9e4d2a10"}'
```

**Response Body**

```json
{
  "issuerId": "did:web:issuer.example.com:0c8f3a52-9b2e-4d31-8a7c-1f6b9e4d2a10",
  "credentialConfigurationsSupported": {}
}
```

* **issuerId** — the Issuer's identifier, equal to the DID you bound it to. This is the `:issuerDid` path segment you'll use when assigning schemas, creating credential offers, and signing credentials.
* **credentialConfigurationsSupported** — the credential configurations this Issuer can issue. It is built from the platform's active schemas — empty on a fresh deployment — and is refreshed when you create and assign schemas. Each schema and format combination becomes a credential configuration id of the form `Name@Version:format`, such as `KYCCredential@1.0:sd-jwt`.

The response can also carry optional `display` metadata (human-readable details wallets can show) when present on the issuer record.

You can fetch the Issuer record at any time with **`GET /agent/issuer/:did`**, list every registered Issuer with **`GET /agent/issuers`**, and retrieve its OpenID4VCI metadata document with **`GET /agent/issuer/:did/.well-known/did-configuration`**.

## Step 3 — Register the Verifier

The verification side follows the same pattern, but runs against your **Verifier** (`$VERIFIER_BASE_URL`) — which in the current One-Click setup shares the Issuer's deployment, though it may run under its own host in other setups, with its own DIDs and admin access token. Create a DID for the Verifier there (repeat Step 1 against `$VERIFIER_BASE_URL` — a separate `did:key` or `did:web` is fine) and register a **Verifier** bound to it. The Verifier signs the authorization requests it sends to wallets with this DID, so holders can confirm who is asking for a presentation.

**`POST /agent/verifier`**

**Request Body**

```json
{
  "verifierId": "did:key:z6MkjFv8Nq2yT4wXa7Yb3Zc9dE1fG2hJ3kL4mN5pQ6rS7t"
}
```

* **verifierId** *(optional)* — the Verifier's DID. If you omit it, the service generates an identifier for you; pass the DID you just created so the Verifier signs its requests with that key.

```bash
curl -X POST "$VERIFIER_BASE_URL/agent/verifier" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"verifierId":"did:key:z6MkjFv8Nq2yT4wXa7Yb3Zc9dE1fG2hJ3kL4mN5pQ6rS7t"}'
```

**Response Body**

```json
{
  "verifierId": "did:key:z6MkjFv8Nq2yT4wXa7Yb3Zc9dE1fG2hJ3kL4mN5pQ6rS7t"
}
```

* **verifierId** — the Verifier's identifier. Keep it: it is the `:verifierId` path segment you'll use when creating authorization requests later in the tutorial ([Deploying the Verifier](/getting-started/tutorial/deploying-verifier) stores it in your `.env` as `VERIFIER_ID`).

You can list every registered Verifier with **`GET /agent/verifiers`**.

## What you have now

You now have two cryptographic identities and the services bound to them:

* An **Issuer** bound to its DID, ready to have schemas assigned and to issue credentials.
* A **Verifier** bound to its DID, ready to request and check presentations.

If you created `did:web` identifiers, their documents are already live at `/<uuid>/did.json` on their respective domains, so any wallet can resolve them without further setup. Next, we'll finish setting up the Issuer deployment itself — saving its base URL and admin access token into our project's `.env`.


# Deploying the Issuer

**What We Are Doing:**

* Using the [One-Click Deployment portal](https://oneclick.empe.io/) to create an Issuer service.
* Saving the values our application needs to talk to that Issuer: its base URL and an admin access token.

**Why:** The Issuer is a trusted authority that creates Verifiable Credentials. Deploying it through the portal provisions a ready-to-use Issuer API (OpenID4VCI). Once it is Active, its `/agent` endpoints are what you use to create a DID and register the Issuer (see [Creating DIDs and Registering Issuer & Verifier](/getting-started/tutorial/creating-dids-and-issuer)), so you can move straight to defining schemas and issuing credentials instead of standing up infrastructure yourself.

**Steps:**

1. Log into the One-Click Deployment platform.
2. Create a new Issuer (see [Create Issuer](/develop/intro/create_issuer)).
3. **Save an admin access token** for the Issuer. Its API is protected by an admin **Bearer (JWT)** token that you send in the `Authorization` header to manage schemas, create credential offers, and sign credentials. Obtain the token from the deployment's Keycloak using the credentials shown after creation — see [Authenticating to Your Deployment](/develop/intro/authenticating-to-your-deployment). Treat it like a password and store it securely; tokens expire, so refresh when needed.
4. Wait until the Issuer status changes to **Active**, then open its **Issuer Details** to note the deployment's base URL (e.g., `https://your-issuer.evdi.app`).

<figure><img src="/files/q0x6fzdbTPKOeTEhFcRz" alt="Issuer Details with the Full Host base URL"><figcaption><p>Issuer Details in the One-Click portal, showing the Full Host (base URL)</p></figcaption></figure>

5. Store these two values in your `.env` file so your application can read them securely:

   ```
   ISSUER_BASE_URL=https://your-issuer.evdi.app
   ISSUER_ACCESS_TOKEN=your-oidc-access-token
   ```

After saving these, they are available in your code as `process.env.ISSUER_BASE_URL` and `process.env.ISSUER_ACCESS_TOKEN`. To use them in your terminal, run `source .env` first.

Every administrative call to the Issuer API carries the access token as a Bearer token:

```bash
curl "$ISSUER_BASE_URL/schemas" \
  -H "Authorization: Bearer $ISSUER_ACCESS_TOKEN"
```

On a fresh deployment the response is an empty list (`[]`) — no schemas exist yet. What matters is that the request was accepted: the same header authorizes every administrative call in this tutorial, from uploading schemas to creating credential offers.

Wherever the Issuer API expects an `:issuerDid` path segment (for example, `POST /issuers/:issuerDid/schemas`), use your Issuer's DID — the `did:web` (a web-hosted DID whose document the service serves over HTTPS) or `did:key` you created and bound to the Issuer in [Creating DIDs and Registering Issuer & Verifier](/getting-started/tutorial/creating-dids-and-issuer). The portal provisions the deployment; creating the DID and registering the Issuer are one-time API steps you run against this deployment (see [Creating DIDs and Registering Issuer & Verifier](/getting-started/tutorial/creating-dids-and-issuer)), and you can list your registered Issuers at any time with `GET /agent/issuers`.

> **Local development:** with `AUTH_DISABLED=true` you can omit the `Authorization` header — see [Authenticating to the API](/getting-started/tutorial/authentication).

You can browse and try every Issuer endpoint — schemas, credential offers, issuance sessions, and direct signing — in your deployment's interactive API reference at **`/api-docs`** (for example, `https://your-issuer.evdi.app/api-docs`).

Next, we will upload a credential schema to let the Issuer know what kind of credentials we want to issue.


# Uploading the Credential Schema

**What We Are Doing:**

* Defining a credential schema that describes the claims our credential will carry (e.g., `age`, `first_name`, `last_name`).
* Creating that schema on the platform, then assigning it to our Issuer so its credential configurations are advertised to wallets.

**Why:** A schema is the contract for a credential. It declares the claim names, their types, and which claims are mandatory, so credentials stay consistent and interoperable. Assigning a schema to an Issuer refreshes the Issuer's OpenID4VCI metadata so wallets can discover the schema's credential configurations.

This is a two-step workflow:

1. **Create the schema** with `POST /schemas` — this registers a reusable, versioned schema on the platform.
2. **Assign the schema** to your Issuer with `POST /issuers/:issuerDid/schemas` — this refreshes the Issuer's OpenID4VCI metadata so the schema's credential configurations are advertised to wallets.

Both endpoints require a valid OIDC Bearer token. Set up your environment first:

```bash
ISSUER_BASE_URL=https://your-issuer.evdi.app
ISSUER_DID=did:web:your-issuer.evdi.app:8f3c...   # your Issuer's DID
TOKEN=<your OIDC access token>                     # any valid OIDC access token (JWT); the tutorial reuses the admin token from the authentication step
```

## Step 1 — Create the schema

Create a `schema.json` file with the schema definition:

```json
{
  "name": "KYCCredential",
  "version": "1.0",
  "description": "Know-Your-Customer verification credential.",
  "formats": ["sd-jwt", "jwt-vc-json"],
  "schema": {
    "type": "object",
    "properties": {
      "age": { "type": "number" },
      "first_name": { "type": "string" },
      "last_name": { "type": "string" }
    },
    "required": ["age", "first_name", "last_name"]
  },
  "vct": "https://your-issuer.evdi.app/vct/kyc-credential",
  "disclosureFrame": ["age", "first_name", "last_name"]
}
```

A few notes on the fields:

* **`name`** and **`version`** are required and must be token-like (matching `^[A-Za-z0-9][A-Za-z0-9._-]*$`) — no spaces. Together they form the schema identifier `name@version` (here, `KYCCredential@1.0`).
* **`schema`** is a JSON Schema describing the claims. Every key listed in `required` must also appear in `properties`.
* **`formats`** is optional; each entry is one of `sd-jwt` or `jwt-vc-json`. If omitted, both formats are enabled by default.
* **`vct`** is the credential type identifier used for SD-JWT VC credentials.
* **`disclosureFrame`** lists the claims a holder can selectively disclose when presenting an SD-JWT VC — the holder can reveal, for example, only `age` without exposing `first_name` or `last_name`.
* For JWT-VC-JSON credentials you can additionally set **`jwtVcTypes`** (e.g., `["VerifiableCredential", "KYCCredential"]`); if you leave it out, sensible defaults are derived from the schema name.

Upload it with `curl`:

```bash
curl -X POST "$ISSUER_BASE_URL/schemas" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d @schema.json
```

The response echoes back the stored schema, including its identifier and lifecycle status:

```json
{
  "schemaId": "KYCCredential@1.0",
  "name": "KYCCredential",
  "version": "1.0",
  "status": "active",
  "schema": { "...": "the definition you sent" },
  "createdAt": "2026-06-22T12:00:00.000Z",
  "updatedAt": "2026-06-22T12:00:00.000Z"
}
```

Schemas are versioned. Creating a new version of an existing `name` (e.g., `KYCCredential@2.0`) automatically marks the previous active version as `superseded`, so the latest version becomes the default. Older versions remain retrievable with `GET /schemas/:schemaId?includeInactive=true` (or via `GET /schemas/:schemaName/versions`).

## Step 2 — Assign the schema to the Issuer

Creating a schema makes it available on the platform. Assigning it refreshes the Issuer's OpenID4VCI metadata so the schema's credential configurations are advertised to wallets. Assign it by posting the schema identifier (`name@version`):

```bash
curl -X POST "$ISSUER_BASE_URL/issuers/$ISSUER_DID/schemas" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "schemaId": "KYCCredential@1.0" }'
```

The response confirms the assignment:

```json
{ "status": "assigned" }
```

## What you get: credential configuration IDs

Each schema combined with a format produces a **credential configuration id** of the form `name@version:format`. Our `KYCCredential@1.0` schema with both formats enabled yields:

* `KYCCredential@1.0:sd-jwt`
* `KYCCredential@1.0:jwt-vc-json`

These configuration ids are what you reference when creating credential offers later in the tutorial. For example, an SD-JWT VC offer for this schema uses `KYCCredential@1.0:sd-jwt`.

With the schema created and assigned, the Issuer now knows exactly what a "KYC Verifiable Credential" looks like. Next, we'll look at the two credential formats and how selective disclosure works before issuing against these configuration ids.


# Credential Formats & Selective Disclosure

**What We Are Doing:**

* Choosing between the two Verifiable Credential formats the platform issues: **SD-JWT VC** and **JWT-VC-JSON**.
* Understanding **credential configuration ids** of the form `name@version:format`, and how a single schema can expose more than one of them.
* Configuring **selective disclosure** for SD-JWT VC credentials with a `disclosureFrame`, so a holder can reveal individual claims without exposing the rest.

**Why:** The format you issue in determines what a credential looks like on the wire and what a holder can do with it when presenting. JWT-VC-JSON is a familiar W3C Verifiable Credential serialized as a JWT — the whole credential is revealed when presented. SD-JWT VC adds *selective disclosure*: each disclosable claim is individually salted and hashed, so the holder can present only the claims a Verifier asks for (for example, `age` without `first_name` or `last_name`). Picking the right format up front means your schema, your credential offers, and your verification requests all line up.

This page assumes you have already created and assigned a schema (see [Uploading the Credential Schema](/getting-started/tutorial/uploading-schema)). The same environment variables apply:

```bash
ISSUER_BASE_URL=https://your-issuer.evdi.app
ISSUER_DID=did:web:your-issuer.evdi.app:8f3c...   # your Issuer's DID
TOKEN=<your OIDC access token>                     # JWT with the admin role
```

Every call below requires a valid OIDC Bearer token.

> **Local development:** with `AUTH_DISABLED=true` you can omit the `Authorization` header — see [Authenticating to the API](/getting-started/tutorial/authentication).

## The two credential formats

The Issuer signs credentials in one of two formats. You select the format at the schema level (which formats a schema enables) and again per credential offer (which configuration id you offer).

* **SD-JWT VC** — a Selective Disclosure JWT Verifiable Credential. In schema configuration this format is `sd-jwt`; in the direct-signing API request you send `sd-jwt-vc`. At runtime the platform identifies these credentials with the format token **`dc+sd-jwt`** — that is the value the signing and verification responses carry, and the value DCQL queries use (the IETF media type for the credential is `application/dc+sd-jwt`). Each disclosable claim is salted and hashed individually, so the holder can present a subset of claims. SD-JWT VC credentials carry a **`vct`** (Verifiable Credential Type) value that identifies the credential type.
* **JWT-VC-JSON** — a W3C Verifiable Credential serialized as a JWT. In schema configuration this format is `jwt-vc-json`; in the direct-signing API it is identified as `jwt_vc_json`. The credential's `type` array (for example `["VerifiableCredential", "KYCCredential"]`) describes what it is. When presented, the credential is revealed in full — there is no per-claim selective disclosure.

|                                    | SD-JWT VC                  | JWT-VC-JSON                       |
| ---------------------------------- | -------------------------- | --------------------------------- |
| Schema `formats` value             | `sd-jwt`                   | `jwt-vc-json`                     |
| Signing API request `format` value | `sd-jwt-vc`                | `jwt_vc_json`                     |
| Runtime response `format` token    | `dc+sd-jwt`                | `jwt_vc`                          |
| Type identifier                    | `vct` (a single string)    | `jwtVcTypes` (a `type` array)     |
| Selective disclosure               | Yes, via `disclosureFrame` | No (whole credential is revealed) |

Use **SD-JWT VC** when you want holders to share only specific claims (the most common choice, and the one used throughout this tutorial). Use **JWT-VC-JSON** when you need a classic W3C VC and full-credential disclosure is acceptable.

## Credential configuration ids: `name@version:format`

A schema is identified by its `name` and `version` (for example `KYCCredential@1.0`). A schema combined with a single format produces a **credential configuration id** of the form `name@version:format`. This is the value you reference when creating credential offers and what wallets discover in the Issuer metadata.

If a schema enables both formats, it exposes two configuration ids — one per format. For a `KYCCredential@1.0` schema with `formats: ["sd-jwt", "jwt-vc-json"]`:

* `KYCCredential@1.0:sd-jwt`
* `KYCCredential@1.0:jwt-vc-json`

The `formats` field controls which configuration ids exist:

```json
{
  "name": "KYCCredential",
  "version": "1.0",
  "description": "Know-Your-Customer verification credential.",
  "formats": ["sd-jwt"],
  "schema": {
    "type": "object",
    "properties": {
      "age":        { "type": "number" },
      "first_name": { "type": "string" },
      "last_name":  { "type": "string" }
    },
    "required": ["age", "first_name", "last_name"]
  },
  "vct": "https://your-issuer.evdi.app/vct/kyc-credential",
  "disclosureFrame": ["age", "first_name", "last_name"]
}
```

The schema above enables only `sd-jwt`, so it yields a single configuration id, `KYCCredential@1.0:sd-jwt`. Omitting `formats` enables **both** formats by default, giving you both `KYCCredential@1.0:sd-jwt` and `KYCCredential@1.0:jwt-vc-json`.

A few format-specific schema fields are worth calling out:

* **`vct`** — used for the SD-JWT VC type. It identifies what the credential *is* and is the value a Verifier matches against when requesting SD-JWT credentials.
* **`jwtVcTypes`** — used for JWT-VC-JSON. It populates the credential's `type` array (for example `["VerifiableCredential", "KYCCredential"]`). If omitted, sensible defaults are derived from the schema name.
* **`disclosureFrame`** — used for SD-JWT VC selective disclosure (explained next). It is ignored for JWT-VC-JSON.

## Selective disclosure with `disclosureFrame`

Selective disclosure is the defining feature of SD-JWT VC. When a credential is issued, every claim listed in its disclosure frame is encoded as a separate, salted *disclosure*. The signed credential body only contains the hashes of those disclosures, so the holder's wallet can choose, at presentation time, which disclosures to reveal. Claims not listed in the disclosure frame are always present in the credential and are not individually hide-able.

There are two places a disclosure frame appears, depending on how you issue.

### In a schema (the OpenID4VCI flow)

When you issue through a credential offer, the disclosure frame comes from the schema. Set **`disclosureFrame`** to the list of claim keys that should be selectively disclosable:

```json
{
  "name": "KYCCredential",
  "version": "1.0",
  "formats": ["sd-jwt"],
  "schema": {
    "type": "object",
    "properties": {
      "age":        { "type": "number" },
      "first_name": { "type": "string" },
      "last_name":  { "type": "string" }
    },
    "required": ["age", "first_name", "last_name"]
  },
  "vct": "https://your-issuer.evdi.app/vct/kyc-credential",
  "disclosureFrame": ["age", "first_name", "last_name"]
}
```

With this configuration, a holder who has claimed a `KYCCredential@1.0:sd-jwt` credential can later present, say, only `age` to a Verifier and keep `first_name` and `last_name` hidden. If you leave `disclosureFrame` out, every claim defined in the schema's `properties` becomes selectively disclosable by default.

Once the schema is created and assigned, you issue exactly as in [Issuing Credentials](/getting-started/tutorial/issuing-credentials) — there is nothing extra to send per offer, because the disclosure frame is already baked into the configuration. The claim values still travel through `issuanceMetadata.claimsByConfigurationId`:

```json
{
  "credentialConfigurationIds": ["KYCCredential@1.0:sd-jwt"],
  "issuanceMetadata": {
    "claimsByConfigurationId": {
      "KYCCredential@1.0:sd-jwt": {
        "age": 30,
        "first_name": "Ada",
        "last_name": "Lovelace"
      }
    }
  }
}
```

### In a direct signing call

For one-off credentials that do not need a wallet/offer flow, the Issuer can sign a credential directly. Here you provide the disclosure frame inline, in the shape **`{ "_sd": [ ... ] }`**.

**`POST {ISSUER_BASE_URL}/issuers/{issuerDid}/credentials/sign`**

Signs a single credential and returns it in compact serialized form. No issuance session or wallet is involved.

**Request Body** (`application/json`):

* **`format`** (string, required) — `sd-jwt-vc` or `jwt_vc_json`.
* **`payload`** (object, required) — the claims to sign. For SD-JWT VC, include a **`vct`** value.
* **`subjectDid`** (string, optional) — the DID of the credential subject; used as the subject identifier in the credential.
* **`disclosureFrame`** (object, optional) — SD-JWT only. An object `{ "_sd": [ ... ] }` whose `_sd` array lists the claim keys that should be selectively disclosable.

**Response Body:**

* **`credential`** — the signed credential in compact serialized form (for SD-JWT VC, the JWT followed by `~`-separated disclosures).
* **`format`** — the resolved, canonical credential-format token (for an SD-JWT VC, `dc+sd-jwt`). Note this is not identical to the `format` enum you send in the request (`sd-jwt-vc`); for a JWT-VC-JSON credential the response token is `jwt_vc`.

Example request:

```bash
curl -X POST "$ISSUER_BASE_URL/issuers/$ISSUER_DID/credentials/sign" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "format": "sd-jwt-vc",
    "payload": {
      "vct": "https://your-issuer.evdi.app/vct/kyc-credential",
      "age": 30,
      "first_name": "Ada",
      "last_name": "Lovelace"
    },
    "disclosureFrame": { "_sd": ["age", "first_name", "last_name"] }
  }'
```

Example response:

```json
{
  "credential": "eyJhbGciOiJFZERTQSJ9.eyJ2Y3QiOi...~WyJzYWx0IiwiYWdlIiwzMF0~",
  "format": "dc+sd-jwt"
}
```

The trailing `~`-separated segments are the disclosures for `age`, `first_name`, and `last_name`. To sign a JWT-VC-JSON credential instead, set `format` to `jwt_vc_json` and omit `disclosureFrame` — that format does not support per-claim disclosure.

## Inspecting what was disclosed

You can decode any credential string — compact JWT or SD-JWT — with the Verifier's direct verification endpoint to confirm which claims it carries.

**`POST {VERIFIER_BASE_URL}/credentials/verify`**

**Request Body:**

* **`credential`** — the compact JWT or SD-JWT string to check.

**Response Body** (when valid):

* **`isValid`** — `true`.
* **`format`** — the detected, canonical format token (for example `dc+sd-jwt` for an SD-JWT VC, `jwt_vc` for a JWT-VC-JSON credential).
* **`payload`** — the decoded credential payload.
* **`prettyClaims`** — for SD-JWT credentials, all disclosed claims merged into a single readable object. (When invalid, the response is `{ "isValid": false, "error": "..." }`, plus the detected `format` whenever the credential could be parsed.)

```bash
curl -X POST "$VERIFIER_BASE_URL/credentials/verify" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "credential": "eyJhbGciOiJFZERTQSJ9.eyJ2Y3QiOi...~WyJzYWx0Iiw..." }'
```

```json
{
  "isValid": true,
  "format": "dc+sd-jwt",
  "payload": { "vct": "https://your-issuer.evdi.app/vct/kyc-credential", "...": "..." },
  "prettyClaims": {
    "vct": "https://your-issuer.evdi.app/vct/kyc-credential",
    "age": 30,
    "first_name": "Ada",
    "last_name": "Lovelace"
  }
}
```

For an SD-JWT VC, `prettyClaims` reflects exactly the disclosures attached to the credential string you passed in — so it is a quick way to verify that selective disclosure is working as expected.

## Matching formats during verification

The format you issue must match the format your Verifier asks for. When you build an OpenID4VP authorization request (see [Creating a Verification Endpoint](/getting-started/tutorial/verify-endpoint)), the credential query targets a specific format and can request individual claim paths — which is what makes selective disclosure usable end to end. For example, as a DCQL query:

```json
{
  "dcqlQuery": {
    "credentials": [
      {
        "id": "kyc_credential",
        "format": "dc+sd-jwt",
        "meta": { "vct_values": ["https://your-issuer.evdi.app/vct/kyc-credential"] },
        "claims": [{ "path": ["age"] }]
      }
    ]
  }
}
```

A Presentation Exchange definition expresses the same targeting with a `vc+sd-jwt` format entry and `limit_disclosure: "required"` — that is the form the tutorial's verification endpoint sends, because it can also constrain claim values (e.g. `age >= 18`). Because the credential was issued as SD-JWT VC with `age` in its disclosure frame, the wallet can answer either request by revealing only `age`. Had the same credential been issued as JWT-VC-JSON, the wallet would have to present the entire credential to satisfy the request.

With formats and selective disclosure understood, you can pick `sd-jwt` for privacy-preserving credentials, scope each credential's `disclosureFrame` to the claims holders should control, and reference the resulting `name@version:format` configuration ids in your offers and verification requests.


# Issuing Credentials

**What We Are Doing:**

* Standing up a minimal Express server (`src/index.js`) that hosts our frontend and mounts an Issuer router.
* Implementing a route (`/kyc-credential`) that asks the Issuer to create a **credential offer** for our `KYCCredential@1.0` schema, then returns the offer data and a QR code the user can scan.

**Why:** This page builds the backend that turns user input into a credential offer. We first need a working Express server, then a route that calls the Issuer's credential-offers endpoint. Scanning the returned QR code with the Empe DID Wallet starts an OpenID4VCI flow in which the wallet claims the credential from the Issuer. We create an offer, and the wallet pulls the credential using the pre-authorized code embedded in it — the Issuer never pushes credentials to a wallet.

## Basic Express Server

**What We Are Doing:**

* Setting up a minimal Express application in `src/index.js` that:
  * Loads environment variables (via `dotenv`).
  * Serves static files from a `public/` directory.
  * Mounts our Issuer router (where the `/kyc-credential` route will live).

**Why:** Before we can create any credential offers, we need a working Express server that can:

* Host our frontend assets (HTML/CSS/JS) under `public/`.
* Accept POST requests (JSON) from the frontend.
* Delegate route handling to `src/issuer-routes.js`.
* Spin up on a known port so the frontend and wallet can reach it.

**Steps:** Create `src/index.js` with the following contents:

```javascript
import express from "express";
import path from "path";
import { fileURLToPath } from "url";
import dotenv from "dotenv";
import issuerRouter from "./issuer-routes.js";

dotenv.config();

// Convert module URL to a __dirname-like value
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// PORT is the local tutorial app's own port (distinct from the deployed Issuer API).
// It comes from .env (fallback to 4000 if not set).
const PORT = process.env.PORT || 4000;

// Initialize Express app
const app = express();

// Serve static assets (index.html, scripts, styles) from public/
app.use(express.static(path.join(__dirname, "../public")));

// Mount our Issuer router (defined in src/issuer-routes.js)
app.use("/issuer", express.json(), issuerRouter);

// Start the server
app.listen(PORT, () => {
  console.log(`Server is running on http://localhost:${PORT}`);
});
```

Make sure your `.env` file holds the values we saved when [deploying the Issuer](/getting-started/tutorial/deploying-issuer) and [uploading the schema](/getting-started/tutorial/uploading-schema):

```
# Base URL of the deployed Issuer
ISSUER_BASE_URL=https://your-issuer.evdi.app

# Your Issuer's DID (read from the Issuer Details page)
ISSUER_DID=did:web:your-issuer.evdi.app:8f3c...

# Admin access token from the deployment's Keycloak, sent as: Authorization: Bearer <token>
ISSUER_ACCESS_TOKEN=your-oidc-access-token

# Port our local tutorial app listens on (its own port, not the deployed Issuer API's)
PORT=4000
```

These are accessible in code as `process.env.ISSUER_BASE_URL`, `process.env.ISSUER_DID`, and `process.env.ISSUER_ACCESS_TOKEN`.

## Creating credential offers

With the server in place, we add a route (`/kyc-credential`) that:

* Takes user input (`age`, `firstName`, `lastName`).
* Asks the Issuer to create a **credential offer** for our `KYCCredential@1.0` schema, with those values as the credential's claims.
* Returns the offer data, including a `credentialOfferUri` and a QR code the user can scan.

Issuance is a single, direct call: you `POST` straight to your Issuer's credential-offers route, referencing the **credential configuration id** you got from the schema (`KYCCredential@1.0:sd-jwt`) and supplying the claim values through `issuanceMetadata`.

## The credential-offers endpoint

**`POST {ISSUER_BASE_URL}/issuers/{issuerDid}/credential-offers`**

Creates a credential offer and an issuance session.

**Request Body** (`application/json`):

* **`credentialConfigurationIds`** (string array, required, at least one) — the credential configurations to offer, in `name@version:format` form, e.g. `["KYCCredential@1.0:sd-jwt"]`.
* **`requirePin`** (boolean, optional, default `false`) — when `true`, the pre-authorized flow also requires a PIN, returned to you so you can hand it to the user out of band.
* **`issuanceMetadata`** (object, optional) — values passed to the credential mapper. Use `claimsByConfigurationId` to supply the actual claim values per configuration id.

**Response Body:**

* **`credentialOfferUri`** — an `openid-credential-offer://...` URI. This is what the wallet consumes (rendered as a QR code or opened as a deep link).
* **`issuanceSessionId`** — identifier of the issuance session you can poll to track progress.
* **`credentialConfigurationIds`** — the configurations included in the offer.
* **`userPin`** — present only when `requirePin` was `true`.

Example request body for our KYC credential:

```json
{
  "credentialConfigurationIds": ["KYCCredential@1.0:sd-jwt"],
  "issuanceMetadata": {
    "claimsByConfigurationId": {
      "KYCCredential@1.0:sd-jwt": {
        "age": 30,
        "first_name": "Ada",
        "last_name": "Lovelace"
      }
    }
  }
}
```

Example response:

```json
{
  "credentialOfferUri": "openid-credential-offer://?credential_offer=...",
  "issuanceSessionId": "d07d0df5-0d64-4f1a-9aa7-7e7f9c2a1d70",
  "credentialConfigurationIds": ["KYCCredential@1.0:sd-jwt"]
}
```

## Implementing the route

**Steps:** Update `src/issuer-routes.js`:

```javascript
import { Router } from "express";
import dotenv from "dotenv";

dotenv.config();

const router = Router();
const ISSUER_BASE_URL = process.env.ISSUER_BASE_URL;
const ISSUER_DID = process.env.ISSUER_DID;
const ISSUER_ACCESS_TOKEN = process.env.ISSUER_ACCESS_TOKEN;

// The credential configuration id is "name@version:format".
// It comes from the schema we created and assigned earlier.
const CONFIG_ID = "KYCCredential@1.0:sd-jwt";

router.post("/kyc-credential", async (req, res) => {
  try {
    const { age, firstName, lastName } = req.body;

    // Create a credential offer for our KYC credential.
    // The claim values are passed via issuanceMetadata.claimsByConfigurationId,
    // keyed by the same credential configuration id we are offering.
    const offerRes = await fetch(
      `${ISSUER_BASE_URL}/issuers/${ISSUER_DID}/credential-offers`,
      {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${ISSUER_ACCESS_TOKEN}`,
        },
        body: JSON.stringify({
          credentialConfigurationIds: [CONFIG_ID],
          issuanceMetadata: {
            claimsByConfigurationId: {
              [CONFIG_ID]: {
                age: Number(age),
                first_name: firstName,
                last_name: lastName,
              },
            },
          },
        }),
      }
    );

    if (!offerRes.ok) {
      const error = await offerRes.text();
      return res.status(offerRes.status).json({ error });
    }

    const offer = await offerRes.json();
    // offer.credentialOfferUri  -> openid-credential-offer://... (what the wallet reads)
    // offer.issuanceSessionId   -> use this to track the session status
    res.status(200).json({
      credentialOfferUri: offer.credentialOfferUri,
      issuanceSessionId: offer.issuanceSessionId,
      // Convenience: a URL our own server exposes that returns a QR PNG for the offer.
      qr_code_url: `/issuer/kyc-credential/${offer.issuanceSessionId}/qr-code`,
    });
  } catch (error) {
    console.error(error);
    res.status(500).json({ error: "Internal server error" });
  }
});

export default router;
```

## Serving a QR code

The wallet needs to scan the `credentialOfferUri`. You can render it as a QR code yourself, or let the Issuer do it for you. The Issuer exposes two PNG endpoints:

* **`POST {ISSUER_BASE_URL}/issuers/{issuerDid}/credential-offers/qr-code`** — takes the *same request body* as the offer endpoint above, but responds with a `400 × 400` PNG image instead of JSON. The offer URI is returned in the **`X-Credential-Offer-Uri`** response header, the session id in **`X-Issuance-Session-Id`**, and (when a PIN is required) the PIN in **`X-Pin`**.
* **`GET {ISSUER_BASE_URL}/issuance-sessions/{sessionId}/qr-code`** — renders a PNG for an offer you already created, looked up by its session id. The offer URI is returned in the **`X-Credential-Offer-Uri`** header.

The second one pairs naturally with the route above: we already have the `issuanceSessionId`, so we can proxy its QR PNG straight to the browser. Add this handler to `src/issuer-routes.js`:

```javascript
router.get("/kyc-credential/:sessionId/qr-code", async (req, res) => {
  try {
    const qrRes = await fetch(
      `${ISSUER_BASE_URL}/issuance-sessions/${req.params.sessionId}/qr-code`,
      { headers: { Authorization: `Bearer ${ISSUER_ACCESS_TOKEN}` } }
    );

    if (!qrRes.ok) {
      return res.status(qrRes.status).json({ error: await qrRes.text() });
    }

    // Stream the PNG back to the browser.
    res.set("Content-Type", "image/png");
    const buffer = Buffer.from(await qrRes.arrayBuffer());
    res.send(buffer);
  } catch (error) {
    console.error(error);
    res.status(500).json({ error: "Internal server error" });
  }
});
```

## Requiring a PIN (optional)

For higher-assurance issuance, set `requirePin: true` in the offer body. The pre-authorized flow will then prompt the wallet for a PIN, and the response (or the `X-Pin` header on the QR endpoint) returns the `userPin` so you can deliver it to the user through a separate channel:

```json
{
  "credentialConfigurationIds": ["KYCCredential@1.0:sd-jwt"],
  "requirePin": true,
  "issuanceMetadata": {
    "claimsByConfigurationId": {
      "KYCCredential@1.0:sd-jwt": { "age": 30, "first_name": "Ada", "last_name": "Lovelace" }
    }
  }
}
```

## Tracking the issuance session

Each offer creates an issuance session you can inspect to see whether the wallet has claimed the credential:

* **`GET {ISSUER_BASE_URL}/issuance-sessions/{sessionId}`** — returns the session, including its `state` (for example `OfferCreated` while waiting, then `Completed` once the wallet claims it), `credentialOfferUri`, `createdAt`, `expiresAt`, and `issuedCredentials` when finished.
* **`GET {ISSUER_BASE_URL}/issuance-sessions?issuerId={issuerDid}`** — lists all sessions for your Issuer.

A quick check from the terminal:

```bash
curl "$ISSUER_BASE_URL/issuance-sessions/d07d0df5-0d64-4f1a-9aa7-7e7f9c2a1d70" \
  -H "Authorization: Bearer $ISSUER_ACCESS_TOKEN"
```

```json
{
  "id": "d07d0df5-0d64-4f1a-9aa7-7e7f9c2a1d70",
  "issuerId": "did:web:your-issuer.evdi.app:8f3c...",
  "state": "OfferCreated",
  "credentialOfferUri": "openid-credential-offer://?credential_offer=...",
  "createdAt": "2026-06-22T12:00:00.000Z",
  "expiresAt": "2026-06-22T12:05:00.000Z"
}
```

## Run the server

```bash
node src/index.js
```

Next, we will build a simple frontend form to submit the user data and display the QR code.


# Frontend for Credential Issuance

**What We Are Doing:**

* Creating a simple HTML form to collect the holder's details.
* When the form is submitted, it calls our backend route, which asks the Issuer for a credential offer and returns the offer data, including a URL for a QR code image.
* The user scans this QR code with a compatible wallet to claim the credential.

**Why:** A user interface is essential so that a real person can enter their details and receive a credential. The browser never talks to the Issuer directly — it talks to our backend, which holds the access token and shapes the request.

**How the QR works:** This is the two-step flow we built on the [Issuing Credentials](/getting-started/tutorial/issuing-credentials) page. First the form `POST`s to our backend route `/issuer/kyc-credential`, which creates the offer at the Issuer and responds with **JSON** — `{ credentialOfferUri, issuanceSessionId, qr_code_url }`. The `qr_code_url` points at our own backend route `GET /issuer/kyc-credential/:sessionId/qr-code`, which proxies the **PNG image** of the QR code. So the frontend reads the POST response as JSON, then simply points an `<img>` at the returned `qr_code_url` to load the QR. The QR encodes an `openid-credential-offer://...` URI; scanning it tells the wallet where to claim the credential.

**Steps:** In `public/index.html`:

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <title>EVDI Tutorial</title>
</head>
<body>
<h1>KYC Credential Issuance</h1>

<form id="issuance-form">
    <label for="firstName">First Name</label>
    <input type="text" name="firstName" id="firstName" required/>

    <label for="lastName">Last Name</label>
    <input type="text" name="lastName" id="lastName" required/>

    <label for="age">Age</label>
    <input type="number" name="age" id="age" required/>

    <button type="submit">Create Credential Offer</button>
</form>

<img src="" alt="Credential offer QR code" width="300" height="300" id="qrcode" style="display:none;"/>

<script>
    const form = document.getElementById('issuance-form');
    const qrcodeImg = document.getElementById('qrcode');

    form.addEventListener('submit', async (event) => {
        event.preventDefault();
        const formData = new FormData(event.target);

        // Field names match what our backend route destructures from req.body.
        const payload = {
            firstName: formData.get('firstName'),
            lastName: formData.get('lastName'),
            age: formData.get('age')
        };

        // Our backend creates the offer at the Issuer and returns JSON,
        // including a qr_code_url pointing at our own QR-proxy route.
        const res = await fetch('/issuer/kyc-credential', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify(payload)
        });

        if (!res.ok) {
            console.error('Failed to create offer:', res.status);
            return;
        }

        // The response is JSON: { credentialOfferUri, issuanceSessionId, qr_code_url }.
        // Point the <img> at qr_code_url to load the QR PNG from our backend.
        const { qr_code_url } = await res.json();
        qrcodeImg.src = qr_code_url;
        qrcodeImg.style.display = 'block';
    });
</script>
</body>
</html>
```

The backend routes are the ones we built on the [Issuing Credentials](/getting-started/tutorial/issuing-credentials) page. The `POST /issuer/kyc-credential` route maps the submitted fields into the offer's `issuanceMetadata.claimsByConfigurationId` for `KYCCredential@1.0:sd-jwt`, calls the Issuer's credential-offers endpoint with the operator's Bearer token, and responds with `{ credentialOfferUri, issuanceSessionId, qr_code_url }`. The browser then loads `qr_code_url` (our `GET /issuer/kyc-credential/:sessionId/qr-code` route), which fetches the Issuer's `GET /issuance-sessions/:sessionId/qr-code` PNG with the same Bearer token and streams it straight back to the `<img>`.

Now you can test issuing a credential by filling out the form and scanning the generated QR code with a compatible wallet. The wallet reads the embedded `openid-credential-offer://` URI, claims the `KYCCredential@1.0:sd-jwt` credential, and stores it.


# Testing Credential Issuance

**What We Are Doing:**

* Running the server.
* Using the reference Empe DID Wallet to claim the issued credential.
* Confirming the credential was claimed by inspecting the issuance session.

**Why:** To make sure the end-to-end issuance flow works before we add the Verifier. A successful test proves that our backend can authenticate to the Issuer API, create a credential offer, and that a wallet can claim the offered credential.

**Before you start:** Two things must be true for the offer to reach the wallet:

* **The server must be authenticated to the Issuer API.** Every call our backend makes to the Issuer carries an OIDC access token as `Authorization: Bearer <token>` (the `ISSUER_ACCESS_TOKEN` from your `.env`). If the token is missing or invalid, the offer call fails and no QR code is produced. (Running locally with `AUTH_DISABLED=true`? Then no token is needed — see [Authenticating to the API](/getting-started/tutorial/authentication).)
* **The offer must be reachable from the phone.** The credential offer is an `openid-credential-offer://` URI that points the wallet back at the Issuer's `ISSUER_BASE_URL`. Your phone resolves that URL directly, so it cannot be `localhost` — use a host the device can reach (a LAN IP or a public HTTPS URL). In production, OpenID4VCI requires HTTPS.

**Steps:**

1. Run the application:

   ```bash
   node src/index.js
   ```
2. Open `http://localhost:4000/` in your browser.
3. Fill out the form (age, firstName, lastName) and submit. Your backend calls the Issuer to create a credential offer and returns a QR code rendering that offer.
4. A QR code appears. Open the Empe DID Wallet on your phone and scan it. The wallet resolves the offer, contacts the Issuer, and stores the issued credential.

## Confirming the credential was claimed

When you created the offer, the Issuer returned an `issuanceSessionId`. You can use it to check the session state and confirm the wallet completed the claim:

**Endpoint:** `GET /issuance-sessions/:sessionId`

```bash
curl https://your-issuer.evdi.app/issuance-sessions/d07d0df5-0d64-4f1a-9aa7-7e7f9c2a1d70 \
  -H "Authorization: Bearer $ISSUER_ACCESS_TOKEN"
```

**Response Body:**

```json
{
  "id": "d07d0df5-0d64-4f1a-9aa7-7e7f9c2a1d70",
  "issuerId": "did:web:your-issuer.evdi.app:8f3c...",
  "state": "Completed",
  "credentialOfferUri": "openid-credential-offer://?credential_offer=...",
  "expiresAt": "2026-06-22T12:34:56.789Z",
  "createdAt": "2026-06-22T12:04:56.789Z",
  "issuedCredentials": ["urn:uuid:abc123"]
}
```

The session starts at `OfferCreated`. Once the wallet claims the credential, `state` becomes `Completed` and `issuedCredentials` lists the issued credential IDs. If something goes wrong, `state` is `Error` and `errorMessage` explains why.

If the QR scan succeeded and the session reports `Completed`, you have successfully issued a credential. Next, we will deploy and integrate the Verifier to check this credential.


# Deploying the Verifier

**What We Are Doing:**

* Using your [One-Click deployment](https://oneclick.empe.io/) — which hosts both the Issuer and Verifier APIs — and registering a verifier identity in the operator console.
* The Verifier will be able to request credential presentations and check that they are valid once we create a verifier identity for it.

**Why:** The Verifier ensures that presented credentials are valid, come from a trusted Issuer, and meet the required criteria. It automates the cryptographic checks and exposes a simple HTTP API so your backend only has to create an authorization request and watch for the result.

**Steps:**

1. Log into the One-Click Deployment platform.
2. Register a verifier identity. The Verifier runs inside your One-Click deployment — a single deployment hosts both the Issuer and Verifier APIs. Register it *either* in the operator console (see [Create Verifier](/develop/intro/create_verifier)) *or* via the API calls under *Create a verifier identity* below — do one, not both.
3. Note the deployment's base URL — the Verifier API is served here. We keep it in its own variable because a Verifier can run under its own host in other setups, even though it currently shares the deployment with the Issuer:
   * `VERIFIER_BASE_URL` (e.g., `https://your-deployment.evdi.app`) — the base URL where the Verifier API is served.
4. **Save an admin access token** for the Verifier. Its API is protected by an admin **Bearer (JWT)** token that you send in the `Authorization` header to create authorization requests and verify presentations. Obtain it from the deployment's Keycloak using the credentials shown after creation — see [Authenticating to Your Deployment](/develop/intro/authenticating-to-your-deployment). Treat it like a password and keep it out of source control.

   > **Local development:** with `AUTH_DISABLED=true` you can omit the `Authorization` header — see [Authenticating to the API](/getting-started/tutorial/authentication).
5. Store these values in your `.env` file so your application can access them securely:

   ```
   VERIFIER_BASE_URL=https://your-deployment.evdi.app
   VERIFIER_ACCESS_TOKEN=your-oidc-access-token
   ```

   After storing these details in `.env`, they are easily accessible in your code using `process.env.VERIFIER_BASE_URL` and `process.env.VERIFIER_ACCESS_TOKEN`, and you can use them in your terminal to interact with the Verifier service (to load them you must run `source .env` first).

**Create a verifier identity:**

Before the service can issue authorization requests, it needs a **verifier** record. Creating one is a one-time setup step — if you already registered a Verifier on this deployment — in the operator console (Step 2) or in [Creating DIDs and Registering Issuer & Verifier](/getting-started/tutorial/creating-dids-and-issuer) — reuse that identifier as your `VERIFIER_ID` below and skip the two API calls. You can create a verifier without a DID and choose the signing DID later (per authorization request), but the simplest setup is to pass a decentralized identifier (DID) as the `verifierId` — the service then uses that DID by default to sign the authorization requests it sends to wallets.

First, create a DID for the verifier. The service can create a `did:key` (a self-contained key-based identifier) or a `did:web` (an identifier whose DID document the service hosts over HTTPS):

**`POST /agent/did/key`**

This request needs no body and returns the new DID:

```bash
curl -X POST "$VERIFIER_BASE_URL/agent/did/key" \
  -H "Authorization: Bearer $VERIFIER_ACCESS_TOKEN"
```

**Response Body**

```json
{
  "did": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
  "verificationMethodIds": ["did:key:z6Mk...#z6Mk..."]
}
```

Then register a verifier, passing that DID as the `verifierId`:

**`POST /agent/verifier`**

**Request Body**

```json
{
  "verifierId": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
}
```

* **verifierId** *(optional)* — an identifier for the verifier, which may be a DID. If you omit it, the service generates one for you. Pass the DID you just created so the service uses it by default to sign authorization requests (you can still override the signing DID per request).

```bash
curl -X POST "$VERIFIER_BASE_URL/agent/verifier" \
  -H "Authorization: Bearer $VERIFIER_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"verifierId":"did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"}'
```

Keep the resulting verifier id — it is the `:verifierId` path segment you'll use when creating authorization requests in the next steps. Store it in your `.env` alongside the values from above:

```
VERIFIER_ID=did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK
```

It is then available in your code as `process.env.VERIFIER_ID`.

Next, we'll configure a verification flow in our backend that uses this verifier to request and check credential presentations.


# Setting Up the Verification Flow

**What We Are Doing:**

* Designing a verification flow we'll call **"kyc"** that asks a wallet to present a KYC credential and checks that:
  * the credential is of the expected type (a KYC credential), and
  * the subject's `age` is at least 18.
* Expressing that requirement in a standard query the Verifier understands, and deciding what our app does once the wallet's presentation is verified.

**Why:** We want only users who hold a valid KYC credential proving they are over 18 to reach a protected resource. The Verifier service does the cryptographic work for us — it builds and signs an **OpenID4VP authorization request**, hands the wallet the requirements, verifies the returned presentation, and tracks the result in a **verification session**. Our job is to describe *what* must be presented and to react to the verified result, not to validate signatures or selective-disclosure ourselves.

A verification flow is simply the request body you send to the Verifier when you create an authorization request: a description of the credential and claims you require. The platform supports two query languages for this — **DIF Presentation Exchange v2** (`presentationDefinition`) and **DCQL** (`dcqlQuery`). You provide exactly one of them.

## Step 1 — Describe the requirement

Let's express the "kyc" flow as a **Presentation Exchange v2** definition: the wallet must present a credential of the KYC type and disclose an `age` claim whose value is at least 18. This is the body you'll send to **`POST /verifiers/:verifierId/authorization-requests`** in the next step.

```javascript
// src/verification-flows.js
// A reusable description of the "kyc" verification flow.
// Sent as the `presentationDefinition` when creating an authorization request.

export const kycPresentationDefinition = {
  id: "kyc",
  input_descriptors: [
    {
      id: "kyc_credential",
      // Restrict to SD-JWT VC credentials of the expected type (vct).
      format: {
        "vc+sd-jwt": { "sd-jwt_alg_values": ["ES256", "EdDSA"] }
      },
      constraints: {
        // Require the holder to actually disclose the matching claims.
        limit_disclosure: "required",
        fields: [
          {
            // The credential type for SD-JWT VC lives in the `vct` claim.
            path: ["$.vct"],
            filter: {
              type: "string",
              const: "https://your-issuer.evdi.app/vct/kyc-credential"
            }
          },
          {
            // The subject must be at least 18.
            path: ["$.age"],
            filter: { type: "number", minimum: 18 }
          }
        ]
      }
    }
  ]
};
```

* **id** — an identifier for the definition; `"kyc"` keeps it readable in logs.
* **input\_descriptors** — one entry per credential you want presented. Here we ask for a single KYC credential.
* **format** — which credential format(s) you accept. We accept `vc+sd-jwt` (SD-JWT VC), the same format the Issuer used earlier in this tutorial.
* **constraints.limit\_disclosure: "required"** — instructs the wallet to disclose only the listed fields, nothing more.
* **constraints.fields** — the actual checks. The first field pins the credential type via the SD-JWT `vct` claim; the second requires `age` to be a number `≥ 18`.

> Prefer DCQL? You can send a `dcqlQuery` instead of `presentationDefinition` — provide **exactly one** of the two; if you send neither, the request is rejected with `Either dcqlQuery or presentationDefinition must be provided.` Note one limitation: DCQL claim queries can request disclosure of `age` and match exact `values`, but they cannot express a numeric range like `age >= 18` — with DCQL your app has to check the disclosed `age` itself after verification. That is why this tutorial uses the Presentation Exchange definition, which enforces the `minimum: 18` filter inside the Verifier.

## Step 2 — Where the flow plugs in

When you create an authorization request, you pass this definition together with a couple of OpenID4VP options. The Verifier returns an `openid4vp://` request URI (to show as a QR code) and a **verification session id** to track:

```javascript
// Body for POST /verifiers/:verifierId/authorization-requests
{
  responseMode: "direct_post.jwt",          // how the wallet returns its response
  version: "v1.draft24",                    // required for presentationDefinition (see below)
  presentationDefinition: kycPresentationDefinition
}
```

* **presentationDefinition** — the "kyc" definition from Step 1.
* **responseMode** *(optional)* — `direct_post`, `direct_post.jwt`, `dc_api`, or `dc_api.jwt`. Defaults to `direct_post.jwt`.
* **version** *(optional)* — `v1`, `v1.draft21`, or `v1.draft24`. Defaults to `v1` — but the default only works with `dcqlQuery`: a request combining `version: "v1"` with a `presentationDefinition` is rejected, so we pass `v1.draft24` here.
* **signingDid** *(optional)* — the DID that signs the authorization request, so wallets can confirm who is asking. If your `:verifierId` path parameter is itself a DID — as in this tutorial, where we registered the verifier under its `did:key` — you can omit `signingDid` and that DID is used. If the verifierId is a plain identifier (like `"verifier-empe"`), `signingDid` is required; without it the request is rejected with *"Missing signing DID. Provide a `signingDid` in the request body when the verifierId is not a DID."*

The body also accepts other OpenID4VP options — `authorizationResponseRedirectUri`, `expectedOrigins` (Digital Credentials API), `transactionData`, and `verifierInfo` — see the API reference for the full list.

The full HTTP call — with the `Authorization: Bearer` token and the frontend QR/SSE wiring — is covered on the next page, [Creating a Verification Endpoint](/getting-started/tutorial/verify-endpoint). Here we only care that the body above *is* the "kyc" flow.

## Step 3 — React to the verified result

You don't poll for "is it done yet". Instead you watch the verification session, either by reading it once with **`GET /verification-sessions/:sessionId`** or by subscribing to the live **`GET /verification-sessions/:sessionId/events`** SSE stream. Each update carries the session's current `state`:

* **RequestCreated** — the request was created and is waiting to be scanned.
* **RequestUriRetrieved** — the wallet fetched the request and is preparing a presentation.
* **ResponseVerified** — the presentation passed every check in your definition (type + `age ≥ 18`). This is the success state.
* **Error** — verification failed; the session's `errorMessage` explains why.

Because the Verifier enforces the constraints before reaching `ResponseVerified`, your code can treat that state as "this user holds a KYC credential and is at least 18" and mint its own short-lived application token. The verification-session record reports only the session *state* — it does **not** include the holder DID or the disclosed claims (`GET /verification-sessions/:sessionId` and the SSE stream return `id`, `verifierId`, `state`, `authorizationRequestId`, `authorizationRequestUri`, `authorizationResponseRedirectUri`, `expiresAt`, and `errorMessage`). If you want the holder DID and the disclosed `age`/`first_name`/`last_name` in your app token (so the dashboard can show them), obtain them yourself — for example by verifying the presented credential string with `POST /credentials/verify`, which returns `{ isValid, format, payload, prettyClaims }`. For SD-JWT VCs issued through the credential-offer flow, the holder binding lives in `payload.cnf` — `cnf.kid` carries the holder's DID verification method (e.g. `did:key:z6Mk...#z6Mk...`); `payload.sub` is set only on directly signed credentials where a `subjectDid` was provided. Sign the values you obtained into your own token:

```javascript
// src/verification-flows.js (continued)
import jwt from "jsonwebtoken";
import dotenv from "dotenv";

dotenv.config();

const JWT_SECRET = process.env.JWT_SECRET; // signs our own app session tokens (set in .env during Project Setup)

// Call this when a verification session reaches "ResponseVerified".
// `holderDid` and `claims` are the values you obtained for the presented
// credential — e.g. from POST /credentials/verify (the holder DID from
// `payload.cnf.kid`, the disclosed claims from `prettyClaims`). The
// verification session itself returns only the session state, not the
// holder DID or disclosed claims (see the note above).
export function onKycVerified(session, { holderDid, claims } = {}) {
  // The Verifier already confirmed the credential type and age >= 18.
  // Issue our own app session token the frontend can use for protected routes.
  // We sign in `sub` (the holder DID) and `claims` so the dashboard can read
  // them back without calling the Verifier again.
  const accessToken = jwt.sign(
    { sub: holderDid, claims, kyc: true, sid: session.id },
    JWT_SECRET,
    { expiresIn: "1h" }
  );

  return {
    access_token: accessToken,
    verification_status: session.state, // "ResponseVerified"
    redirect_url: "/authorization/dashboard"
  };
}
```

The session object you receive looks like this:

```json
{
  "id": "18b760b1-4ab8-4d38-9d42-6b4c25a4d2e0",
  "verifierId": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
  "state": "ResponseVerified",
  "authorizationRequestId": "1b8a6a8b-2ac1-4f0b-9d9c-6b9c859b1af2",
  "authorizationRequestUri": "openid4vp://?request_uri=...",
  "expiresAt": "2024-08-09T12:34:56.789Z"
}
```

Notice the session record reports the *state* of the verification, not the disclosed claims. For our gate that's enough — we only needed to know the presentation satisfied the "kyc" requirements. The holder's DID will be a `did:key` or `did:web` — the methods wallets in this ecosystem use.

Now our app has everything it needs: a precise description of the "kyc" flow to send to the Verifier, and a clear rule for what to do once a session reaches `ResponseVerified`. Next, we'll wire this into a real endpoint that requests a QR code and watches the session in real time.


# Creating a Verification Endpoint

**What We Are Doing:**

* Adding a backend route (e.g., `/kyc-authorize`) that asks the Verifier to create an authorization request carrying our "kyc" presentation definition, and returns its QR data to the frontend.
* Displaying the verification QR code so the user can scan it with the Empe DID Wallet.
* Using SSE (Server-Sent Events) to receive real-time updates as the verification session changes state — through a small backend proxy that adds the Bearer token.
* Exchanging the verified session for our app's own session token, so the protected dashboard can open.

**Why:** To start a verification, our app asks the Verifier to create an **OpenID4VP authorization request**. The Verifier returns an `openid4vp://` request URI (which we render as a QR code) and a **verification session id**. The user scans the QR with their wallet, the wallet presents the requested credential, and the Verifier checks it. We watch the session over SSE and, once it reaches the verified state, mint our app session token and redirect the user to a protected resource.

**Steps:** Add the following to `src/verifier-routes.js`. This route calls the Verifier service (not our app) with a Bearer token and returns the authorization request URI and session id to the frontend. It sends the `kycPresentationDefinition` we built on the [previous page](/getting-started/tutorial/verification-flow) — that definition is what enforces the credential type and `age >= 18`:

```javascript
import { Router } from "express";
import dotenv from "dotenv";
import { Readable } from "node:stream";
import {
  kycPresentationDefinition,
  onKycVerified,
} from "./verification-flows.js";

dotenv.config();

const router = Router();
const VERIFIER_BASE_URL = process.env.VERIFIER_BASE_URL;
const VERIFIER_ACCESS_TOKEN = process.env.VERIFIER_ACCESS_TOKEN;
const VERIFIER_ID = process.env.VERIFIER_ID; // the verifier id from the deploy step

router.post("/kyc-authorize", async (req, res) => {
  try {
    // Ask the Verifier to create an OpenID4VP authorization request.
    // The presentation definition asks the wallet for a KYCCredential SD-JWT
    // credential and enforces age >= 18. Note the version: a
    // presentationDefinition requires "v1.draft24" (the default "v1" only
    // works with dcqlQuery).
    const response = await fetch(
      `${VERIFIER_BASE_URL}/verifiers/${VERIFIER_ID}/authorization-requests`,
      {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${VERIFIER_ACCESS_TOKEN}`,
        },
        body: JSON.stringify({
          responseMode: "direct_post.jwt",
          version: "v1.draft24",
          presentationDefinition: kycPresentationDefinition,
        }),
      }
    );

    const data = await response.json();
    // data = { authorizationRequestUri, verificationSessionId, authorizationRequestId?, expiresAt? }
    res.status(200).json(data);
  } catch (error) {
    console.error(error);
    res.status(500).json({ error: "Internal server error" });
  }
});

export default router;
```

**Request Body** (sent to **`POST /verifiers/:verifierId/authorization-requests`**)

```json
{
  "responseMode": "direct_post.jwt",
  "version": "v1.draft24",
  "presentationDefinition": {
    "id": "kyc",
    "input_descriptors": [
      {
        "id": "kyc_credential",
        "format": { "vc+sd-jwt": { "sd-jwt_alg_values": ["ES256", "EdDSA"] } },
        "constraints": {
          "limit_disclosure": "required",
          "fields": [
            {
              "path": ["$.vct"],
              "filter": { "type": "string", "const": "https://your-issuer.evdi.app/vct/kyc-credential" }
            },
            {
              "path": ["$.age"],
              "filter": { "type": "number", "minimum": 18 }
            }
          ]
        }
      }
    ]
  }
}
```

* **presentationDefinition** — the "kyc" definition from the previous page: it pins the credential type via `vct` and requires the disclosed `age` to be at least 18. Provide **either** `presentationDefinition` **or** `dcqlQuery` — exactly one is required. (A DCQL query can request disclosure of `age` but cannot express `age >= 18`; if you use DCQL, your app must check the disclosed value itself.)
* **responseMode** *(optional)* — how the wallet returns its response: `direct_post`, `direct_post.jwt`, `dc_api`, or `dc_api.jwt`. Defaults to `direct_post.jwt`.
* **version** *(optional)* — OpenID4VP draft to use: `v1`, `v1.draft21`, or `v1.draft24`. Defaults to `v1`, which only works with `dcqlQuery` — a `presentationDefinition` requires `v1.draft24` (or `v1.draft21`).

**Response Body**

```json
{
  "authorizationRequestUri": "openid4vp://?request_uri=...",
  "verificationSessionId": "18b760b1-4ab8-4d38-9d42-6b4c25a4d2e0",
  "authorizationRequestId": "1b8a6a8b-2ac1-4f0b-9d9c-6b9c859b1af2",
  "expiresAt": "2024-08-09T12:34:56.789Z"
}
```

* **authorizationRequestUri** — the `openid4vp://` request URI to encode as a QR code.
* **verificationSessionId** — the session id we'll subscribe to for live updates.
* **authorizationRequestId** *(optional)* — identifier of the hosted request.
* **expiresAt** *(optional)* — when the request expires (ISO 8601).

> If you prefer the Verifier to render the QR for you, call **`POST /verifiers/:verifierId/authorization-requests/qr-code`** with the same body. It returns a **PNG image**, and the request URI and session id come back in the `X-Authorization-Request-Uri` and `X-Verification-Session-Id` response headers.

## Proxying the session event stream

The Verifier's session-events endpoint (`GET /verification-sessions/:sessionId/events`) requires a Bearer token like every other non-public route, and a browser `EventSource` cannot attach an `Authorization` header. So the frontend watches the stream through our own backend: a small proxy route that forwards the events and adds the token server-side. Add it to `src/verifier-routes.js`:

```javascript
// Browsers can't send an Authorization header from EventSource, so we proxy
// the Verifier's SSE stream and attach the Bearer token server-side.
router.get("/kyc-sessions/:sessionId/events", async (req, res) => {
  try {
    const upstream = await fetch(
      `${VERIFIER_BASE_URL}/verification-sessions/${req.params.sessionId}/events`,
      {
        headers: {
          Authorization: `Bearer ${VERIFIER_ACCESS_TOKEN}`,
          Accept: "text/event-stream",
        },
      }
    );

    if (!upstream.ok || !upstream.body) {
      return res.status(upstream.status).json({ error: await upstream.text() });
    }

    res.set({
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache",
      Connection: "keep-alive",
    });
    res.flushHeaders();

    // Pipe the upstream SSE bytes straight through to the browser.
    const stream = Readable.fromWeb(upstream.body);
    stream.pipe(res);
    res.on("close", () => stream.destroy()); // stop when the browser disconnects
  } catch (error) {
    console.error(error);
    res.status(500).json({ error: "Internal server error" });
  }
});
```

The proxied stream emits exactly what the Verifier sends: `session` events carrying the session object, plus a `ping` heartbeat every 15 seconds that keeps the connection alive.

## Exchanging the verified session for an app token

Once the session reaches `ResponseVerified`, the frontend needs the app session token that the protected dashboard checks — minted by the `onKycVerified` helper from [Setting Up the Verification Flow](/getting-started/tutorial/verification-flow). Add a route that re-reads the session server-side (so a client cannot fake the state) and returns the token:

```javascript
// Exchange a verified session for our app's own session token.
router.post("/kyc-sessions/:sessionId/token", async (req, res) => {
  try {
    const sessionRes = await fetch(
      `${VERIFIER_BASE_URL}/verification-sessions/${req.params.sessionId}`,
      { headers: { Authorization: `Bearer ${VERIFIER_ACCESS_TOKEN}` } }
    );
    if (!sessionRes.ok) {
      return res.status(sessionRes.status).json({ error: await sessionRes.text() });
    }

    const session = await sessionRes.json();
    if (session.state !== "ResponseVerified") {
      return res.status(409).json({ error: `Session state is ${session.state}` });
    }

    // The session proves the "kyc" checks passed (credential type + age >= 18),
    // but it does not carry the holder DID or the disclosed claims. Pass them
    // here if your app obtained them (e.g. via POST /credentials/verify) so
    // the dashboard can display them; without them the dashboard still opens,
    // with those fields empty.
    res.json(onKycVerified(session, {}));
  } catch (error) {
    console.error(error);
    res.status(500).json({ error: "Internal server error" });
  }
});
```

## Mount the router

The routes above live in `src/verifier-routes.js`, but our Express app doesn't serve them yet. Update `src/index.js` to import and mount the router under the `/verifier` prefix, next to the issuer router from [Issuing Credentials](/getting-started/tutorial/issuing-credentials):

```javascript
// src/index.js — add the import at the top:
import verifierRouter from "./verifier-routes.js";

// ...and mount it below the issuer router:
app.use("/verifier", express.json(), verifierRouter);
```

## Frontend: button, QR code and SSE

The frontend gets the `authorizationRequestUri` and `verificationSessionId` from our route, renders the URI as a QR code, then opens an SSE connection to watch the verification session. In `public/index.html`, at the bottom of `body`, add a button to start verification:

```html
<button id="kyc-authorize-btn">Authorize with KYC Credential</button>
<img id="verifier-qrcode" style="display: none;" alt="Scan to verify" />

<!-- A small library to turn the openid4vp:// URI into a QR image in the browser -->
<script src="https://cdn.jsdelivr.net/npm/qrcode/build/qrcode.min.js"></script>
<script>
    const authorizeBtn = document.getElementById('kyc-authorize-btn');
    const qrcodeImg = document.getElementById('verifier-qrcode');

    authorizeBtn.addEventListener('click', async () => {
        // 1. Ask our backend to create the authorization request via the Verifier.
        const res = await fetch('/verifier/kyc-authorize', { method: 'POST' });
        const { authorizationRequestUri, verificationSessionId } = await res.json();

        // 2. Render the openid4vp:// request URI as a QR code for the wallet to scan.
        qrcodeImg.src = await QRCode.toDataURL(authorizationRequestUri);
        qrcodeImg.style.display = 'block';

        // 3. Open an SSE connection through our backend proxy, keyed by the
        //    session id. The stream emits `session` events (the session object)
        //    plus a `ping` heartbeat every 15 seconds to keep the connection alive.
        const eventSource = new EventSource(
            `/verifier/kyc-sessions/${verificationSessionId}/events`
        );

        // 4. Each `session` event carries the current session state. Wait for the
        //    verification to complete.
        eventSource.addEventListener('session', async (event) => {
            const session = JSON.parse(event.data);
            console.log('Verification session state:', session.state);

            if (session.state === 'ResponseVerified') {
                // Credential was presented and verified successfully.
                eventSource.close();

                // 5. Exchange the verified session for our app's session token,
                //    store it, then open the protected dashboard.
                const tokenRes = await fetch(
                    `/verifier/kyc-sessions/${verificationSessionId}/token`,
                    { method: 'POST' }
                );
                const { access_token, redirect_url } = await tokenRes.json();
                localStorage.setItem('access_token', access_token);
                window.location.href = redirect_url; // "/authorization/dashboard"
            } else if (session.state === 'Error') {
                // Verification failed; session.errorMessage explains why.
                console.error('Verification failed:', session.errorMessage);
                eventSource.close();
            }
        });
    });
</script>
```

The session moves through a small set of states you can react to:

* **RequestCreated** — the authorization request was created and is waiting to be scanned.
* **RequestUriRetrieved** — the wallet fetched the request and is preparing a presentation.
* **ResponseVerified** — the wallet's presentation passed all checks. This is the success state.
* **Error** — verification failed; `errorMessage` describes the problem.

Each SSE `session` event contains the full session object, for example:

```json
{
  "id": "18b760b1-4ab8-4d38-9d42-6b4c25a4d2e0",
  "verifierId": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
  "state": "ResponseVerified",
  "authorizationRequestId": "1b8a6a8b-2ac1-4f0b-9d9c-6b9c859b1af2",
  "authorizationRequestUri": "openid4vp://?request_uri=...",
  "expiresAt": "2024-08-09T12:34:56.789Z"
}
```

> **Auth note:** The frontend never talks to the Verifier directly — the `EventSource` points at our backend proxy (`/verifier/kyc-sessions/:sessionId/events`), which holds the `VERIFIER_ACCESS_TOKEN` and forwards the stream. This works against any deployment. (Only against a Verifier running locally with `AUTH_DISABLED=true` could a page open `EventSource` on `VERIFIER_BASE_URL` directly, since a browser `EventSource` cannot attach an `Authorization` header.)

With this in place, clicking "Authorize with KYC Credential" starts the verification flow: the user scans the QR, presents their credential, and once the session reaches `ResponseVerified` the frontend stores the app session token and is redirected to `/authorization/dashboard`. In the next step we'll build that protected dashboard.


# Creating a Protected Dashboard

**What We Are Doing:**

* Adding a protected resource (served at `/authorization/dashboard`) that only verified users can access.
* Decoding the app session token to display the claims that were disclosed during verification.

**Why:** To show the end result of verification, we'll display a dashboard page that uses the credential data the holder presented. After a successful verification, our backend issues a short-lived session token (a symmetric JWT we sign ourselves) — the `/verifier/kyc-sessions/:sessionId/token` route from [Creating a Verification Endpoint](/getting-started/tutorial/verify-endpoint), which calls the `onKycVerified` helper from [Setting Up the Verification Flow](/getting-started/tutorial/verification-flow). The dashboard reads that token and renders whatever the verification flow chose to embed in it — no further calls to the Verifier are needed.

The verification session returns only session metadata (`id`, `verifierId`, `state`, `authorizationRequestId`, `authorizationRequestUri`, `authorizationResponseRedirectUri`, `expiresAt`, `errorMessage`). To show the holder DID and claims, your flow must obtain them — e.g. via `POST /credentials/verify`, which returns the credential `payload` (holder binding in `payload.cnf`) and `prettyClaims` — and sign them into the token as `sub` and `claims`, as `onKycVerified` does when you pass them. Otherwise those dashboard fields stay empty.

**Steps:** Create `public/dashboard.html`:

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <title>Dashboard</title>
</head>
<body>
<h2>Welcome to the Dashboard!</h2>
<p>DID: <span id="did"></span></p>
<p>First Name: <span id="first-name"></span></p>
<p>Last Name: <span id="last-name"></span></p>
<p>Age: <span id="age"></span></p>

<script>
    (async () => {
        const token = localStorage.getItem("access_token");
        if (!token) {
            window.location.href = "/";
            return;
        }

        const res = await fetch('/authorization/decode-access-token', {
            headers: { Authorization: `Bearer ${token}` }
        });
        if (!res.ok) {
            // Token missing, expired, or invalid — send the user back to verify again.
            localStorage.removeItem("access_token");
            window.location.href = "/";
            return;
        }

        // The DID/claim fields are filled only if the verification flow
        // signed them into the token (see the hint above).
        const data = await res.json();
        const claims = data.claims ?? {};
        document.getElementById("did").innerText = data.did ?? "—";
        document.getElementById("first-name").innerText = claims.first_name ?? "—";
        document.getElementById("last-name").innerText = claims.last_name ?? "—";
        document.getElementById("age").innerText = claims.age ?? "—";
    })();
</script>
</body>
</html>
```

Add a route to decode the session token and extract the claims in `src/authorization-routes.js`. It verifies the token with the `JWT_SECRET` you set in your `.env` during [Project Setup](/getting-started/tutorial/project-setup):

```javascript
import { Router } from "express";
import jwt from "jsonwebtoken";
import dotenv from "dotenv";

dotenv.config();

const router = Router();
const JWT_SECRET = process.env.JWT_SECRET; // Secret used to sign/verify our own session token

router.get("/decode-access-token", (req, res) => {
    const authHeader = req.headers["authorization"];
    const access_token = authHeader?.split(" ")[1];
    if (!access_token) return res.status(401).json({ error: "Unauthorized" });

    try {
        // Verify our own session token. Because we signed it with `exp`,
        // jwt.verify() also rejects expired tokens automatically.
        const decoded = jwt.verify(access_token, JWT_SECRET);

        // We read the holder DID and disclosed claims straight from the payload.
        // These fields are present only if the verification flow signed them in
        // (see the hint at the top of this page).
        res.json({
            did: decoded.sub,        // the holder's did:key / did:web (if signed in)
            claims: decoded.claims,  // disclosed claims, e.g. { first_name, last_name, age } (if signed in)
        });
    } catch (error) {
        // Invalid signature or expired token.
        return res.status(401).json({ error: "Invalid or expired token" });
    }
});

router.get("/dashboard", (req, res) => {
    res.sendFile("dashboard.html", { root: "public" });
});

export default router;
```

Finally, mount this router in `src/index.js` under the `/authorization` prefix, next to the issuer and verifier routers:

```javascript
// src/index.js — add the import at the top:
import authorizationRouter from "./authorization-routes.js";

// ...and mount it with the other routers:
app.use("/authorization", express.json(), authorizationRouter);
```

Now, after verification, the user is redirected to `/authorization/dashboard`, and the dashboard displays what the verification flow signed into the session token.

A few things to keep in mind:

* **Match the claim keys to your schema.** This example reads `first_name`, `last_name`, and `age` — the claims of the `KYCCredential@1.0:sd-jwt` credential that the "kyc" flow in [Setting Up the Verification Flow](/getting-started/tutorial/verification-flow) requires (`age ≥ 18`). Use the exact property names from the credential schema you are verifying; if you verify a different credential, render its own claim keys instead.
* **The DID shown is the holder's DID** — the `did:key` or `did:web` of the wallet that presented the credential, read from the session token's `sub` claim.
* **The disclosed claims come from the verified presentation.** Pass through only the claims the holder chose to disclose (SD-JWT selective disclosure), so the dashboard never sees more than what was presented.

> **Best practice:** Give the app session token a short lifetime. The `onKycVerified` helper signs it with `expiresIn: "1h"` — tighten that (e.g. `"15m"`) for sensitive resources. `jwt.verify()` then rejects expired tokens for you, so a leaked token can't grant access to the protected dashboard indefinitely. This session token is internal to your app and is unrelated to the OIDC access token you use to call the Issuer and Verifier APIs.


# Testing the Verification Flow

**What We Are Doing:**

* Running through the entire process end to end: issuance, verification, and accessing the dashboard.

**Why:** To ensure all components work together smoothly, from claiming a credential into the wallet to satisfying a verification request and reaching a protected resource.

**Steps:**

1. Start the server:

   ```bash
   node src/index.js
   ```
2. Issue a credential by submitting the form and scanning the issuance QR code with the Empe DID Wallet. The credential is now stored in the wallet.
3. Click "Authorize with KYC Credential" to create an authorization request and display the verification QR code.
4. Scan the verification QR code with the wallet. The credential requirement (a `KYCCredential@1.0:sd-jwt` credential of the expected type whose `age` is at least 18) is enforced by the `presentationDefinition` carried in the authorization request itself — the wallet only offers credentials that satisfy it, so a holder without a matching credential has nothing to present. (If you switch to a `dcqlQuery` instead, remember it cannot express `age >= 18` — your app must then check the disclosed `age` itself.)
5. When the wallet sends a matching presentation, the verification session advances to `ResponseVerified`. Your frontend observes this in real time over the proxied session events stream, exchanges the session for the app session token, stores it, and redirects you to `/authorization/dashboard`. The dashboard's DID and claim fields show values only if your flow signed them into the token (see [Creating a Protected Dashboard](/getting-started/tutorial/dashboard)).

If everything works, you've successfully completed the end-to-end flow — the credential was issued, presented, verified against your request, and the verified result was delivered through the verification session.


# Summary & Next Steps

**What You Achieved:**

* Deployed an Issuer and Verifier using the One-Click Deployment platform, and authenticated to their APIs with an OIDC `Authorization: Bearer <token>` header (using `AUTH_DISABLED=true` as the local-development shortcut).
* Created a versioned credential schema and assigned it to your Issuer, producing the credential configuration ids your Issuer can advertise (for example `KYCCredential@1.0:sd-jwt`).
* Issued a "KYC Verifiable Credential" by creating an **OpenID4VCI** credential offer and claiming it into a wallet — in **SD-JWT VC** format, so the holder can selectively disclose individual claims.
* Verified that credential with an **OpenID4VP** authorization request that declares which claims it needs, then read the result from the verification session once the holder presented a matching credential.
* Built a frontend that lets users request a credential, scan the offer QR code, and later present their credential to unlock a protected dashboard.
* Displayed the verified, selectively disclosed credential data on that protected dashboard.

**Next Steps:**

* **Try selective disclosure.** Add or adjust the `disclosureFrame` on your schema (or the `disclosureFrame` on a direct sign request) so a holder can reveal only `age` without exposing `first_name` or `last_name`.
* **Switch the credential query.** Authorization requests accept either a DIF **Presentation Exchange** `presentationDefinition` (as this tutorial sends, paired with `version: "v1.draft24"`) or a **DCQL** `dcqlQuery` (works with the default `version: "v1"`). Note DCQL cannot express numeric ranges like `age >= 18` — with DCQL your app checks the disclosed value itself.
* **Require a PIN on offers.** Set `requirePin: true` when creating a credential offer to add a pre-authorized-code PIN that the holder must enter before claiming.
* **Issue a different format.** The same schema can produce both `:sd-jwt` and `:jwt-vc-json` credential configuration ids — try issuing a **JWT-VC-JSON** credential alongside the SD-JWT VC one.
* **Experiment with schemas and conditions.** Define new schemas with different claims and stricter verification requirements, then use the verified credential data to offer personalized experiences in your application.

You now have a solid understanding and a working example of SSI credential issuance and verification within the Empeiria ecosystem.


# EVDI Architecture

The **End-to-End Verifiable Data Infrastructure (EVDI)** provides a standards-based platform for managing decentralized digital identities and Verifiable Credentials (VCs). It is composed of multiple subsystems, each covering a distinct role in the Self-Sovereign Identity (SSI) lifecycle. Across the platform, identifiers are created and resolved by the service itself, credentials are signed off-chain, and holders decide exactly what to share.

## Core Components

### 1. One Click Deployment

One Click Deployment streamlines the setup and configuration of **Issuer** and **Verifier** services. By automating the deployment process, it eliminates much of the manual complexity involved in standing up secure SSI environments. Users gain:

* **Rapid Provisioning**: Instantly deploy Issuers and Verifiers, each registered against a service-created DID, with no manual key or DID-document handling.
* **Centralized Management**: Easily access, monitor, and maintain deployed components through a unified dashboard.

### 2. Issuer Service

The **Issuer Service** is responsible for creating and delivering Verifiable Credentials using the **OpenID4VCI** protocol over a standards-based HTTP API. Key features include:

* **DID Management**: Creates and serves its own **`did:web`** documents over HTTPS and **`did:key`** identifiers, using Ed25519 keys.
* **Schema Management**: Defines credential structure with JSON-Schema templates and supports versioning. A schema together with a format yields a **credential configuration id** of the form `Name@Version:format` (for example, `EmployeeBadge@1:sd-jwt`).
* **Credential Issuance**: Issues credentials in **SD-JWT VC** (`vc+sd-jwt`) and **JWT-VC-JSON** (`jwt_vc_json`) formats, cryptographically signed with Ed25519 keys held in the service's internal key store.
* **Credential Offers**: Creates `openid-credential-offer://` offers (optionally rendered as a QR code) that wallets claim through the OpenID4VCI **pre-authorized code flow**, with an optional PIN.
* **Direct Signing**: Exposes a server-to-server endpoint that returns a signed credential without a wallet round-trip.

### 3. Verifier Service

The **Verifier** requests and validates Verifiable Presentations from wallets and checks the credentials inside them, using the **OpenID4VP** protocol. Its responsibilities include:

* **Credential Verification**: Validates cryptographic signatures against the issuer's published DID document and checks compliance with the requested claims — no central registry lookup is required.
* **Flexible Queries**: Requests exactly the credentials and claims needed using either a **DCQL** query or a **DIF Presentation Exchange v2** definition.
* **Real-Time Feedback**: Each request creates a **verification session** whose state can be polled or subscribed to over **Server-Sent Events (SSE)** for live updates as the wallet responds.
* **Standalone Verification**: Beyond the full presentation flow, a direct endpoint verifies a single compact credential string (SD-JWT or JWT-VC) and returns its decoded claims.

### 4. Holder Wallets

Holders store their DIDs and credentials and present them to verifiers. The platform supports two complementary holder options:

* **Empe DID Wallet**: A reference mobile wallet application available for iOS and Android. It lets holders securely manage DIDs and credentials, receive Verifiable Credentials, and deliver signed presentations to verifiers.
* **Cloud Wallet**: A server-side, multi-tenant holder built into the same backend. It speaks the same OpenID4VCI and OpenID4VP protocols a mobile wallet uses, so credential-offer and presentation-request URIs can be claimed and presented over plain HTTP — ideal for automated tests, CI pipelines, and agents acting as a holder programmatically.

Both interact with Issuers and Verifiers through the same standards-based flows, so a credential issued to a mobile wallet and one claimed by the cloud wallet are verified identically.

## Identifiers and Trust

DIDs and their documents are created and resolved by the service itself, with no on-chain registration step. The platform issues two methods:

* **`did:web`** — the DID document is self-hosted by the service and served over HTTPS, resolvable at `/:uuid/did.json`. Resolving the DID simply means fetching that document.
* **`did:key`** — a deterministic, self-describing identifier whose public key is encoded directly in the DID, so no hosting is required.

A DID document lists the public keys and verification methods needed to validate the credentials and presentations associated with that identity. This makes trust **self-contained**: a verifier establishes authenticity by checking the issuer's signature against the public key published in its DID document — no central authority, ledger query, or registry lookup is involved.

The EMPE DID method (`did:empe`) and EMPE blockchain exist in the wider Empeiria ecosystem to provide shared, tamper-evident trust infrastructure. Within this platform, however, issuance, holding, and verification are performed off-chain: DID documents and schemas are stored by the service, credentials live in the holder's wallet, and authenticity is established through cryptographic signatures and selective disclosure rather than by querying a ledger.

## Bringing It All Together

**One Click Deployment** kickstarts your ecosystem by provisioning fully configured Issuer and Verifier services. The **Issuer Service** offers credentials to wallets over OpenID4VCI, and the **Verifier Service** requests and checks presentations over OpenID4VP. Both resolve DIDs directly — fetching `did:web` documents over HTTPS or decoding `did:key` identifiers — to verify signatures without central intermediaries.

This architecture forms a complete SSI environment:

1. **Issuer** defines schemas, signs credentials, and offers them to wallets.
2. **Holder Wallets** (the Empe DID Wallet on mobile, or the server-side cloud wallet) let individuals and agents store and present their own credentials.
3. **Verifier** requests presentations and validates credentials against the claims you require, with live session feedback over SSE.

With **selective disclosure** via SD-JWT, holders reveal only the specific claims a verifier needs while the issuer's signature over the whole credential still validates. All operator endpoints are protected by **OIDC JWT Bearer** authentication; the `admin` role is required only on the agent/DID and wallet-management routes.

By adhering to W3C Verifiable Credentials and DID Core standards, and by building on OpenID4VCI and OpenID4VP, the EVDI ecosystem keeps your SSI solutions interoperable, future-proof, and user-centric.


# Self-Sovereign Identity

Empeiria's End-to-End Verifiable Data Infrastructure (EVDI) builds on the foundation of Self-Sovereign Identity (SSI), expanding its utility and enabling wider adoption.

SSI is a concept in digital identity, where individuals have full ownership and control over their personal identity information. Unlike traditional identity systems relying on centralized authorities, SSI allows users to own, manage, and share their data. This approach enhances privacy, security, and user autonomy by enabling individuals to authenticate themselves without depending on external entities.

In practice, the issuing, holding, and verifying of credentials in this platform happen off-chain: identities are expressed as Decentralized Identifiers (DIDs), credentials are protected by cryptographic signatures, and minimal disclosure is achieved through selective-disclosure techniques. Personal data stays in the holder's wallet, and verification is cryptographic.

This platform issues and verifies credentials off-chain with `did:web` and `did:key`. The `did:empe` method and EMPE blockchain are separate components of the wider Empeiria ecosystem — see [EVDI Architecture](/understand/intro).

## Key Features of SSI <a href="#id-37e1" id="id-37e1"></a>

Here are the key features and principles of SSI:

* **User-Centric:** In SSI, users have access and control over their identity data. They can decide what information to share, with whom, and for how long.
* **Privacy-Enhancing:** SSI is designed to prioritize privacy. Using cryptographic selective-disclosure techniques (SD-JWT), a holder can prove specific facts—such as a single claim from a credential—while keeping the rest of their data hidden. Disclosure of claims is minimized so users reveal only what each interaction requires.
* **Decentralized:** Traditional identity systems are controlled by central authorities, while SSI is built on decentralized principles. Identities are expressed as DIDs, the holder keeps all their data within the wallet, and no central registry sits between issuer, holder, and verifier.
* **Interoperable:** SSI systems aim to be universally applicable, allowing users to use their identity across different services and platforms without separate credentials.
* **Secure:** These systems use advanced cryptographic techniques—digital signatures over each credential—so that identity data is tamper-evident and can be verified independently, without contacting the original issuer.
* **Consent-Based:** Any sharing of personal data in SSI systems is based on the user's explicit consent, providing greater control over personal data.
* **Portable:** Users can carry their identity across different platforms and services without depending on a single provider.
* **Transparent:** The protocols and software are open, so anyone can inspect how identity data is handled and independently verify that the system behaves as claimed.


# Technical Foundations

Self-Sovereign Identity (SSI) relies on a blend of cryptographic techniques and open standards to empower user control and decentralize identity management. On the EVDI platform, these foundations are applied in a concrete way: identifiers are created and resolved by the service, credentials are signed off-chain, and holders decide exactly what to share. Here's a breakdown of the key innovations that make self-sovereign identity possible:

**Public-Key Cryptography:** This creates a key pair — a public key for sharing information and a private key for secure access. VCs are signed with the issuer's private key, and anyone can verify the signature using the corresponding public key. On the platform, signing keys (Ed25519) are generated and held in a backend key store rather than recorded on any ledger; the matching public keys are published in the issuer's DID document so verifiers can check signatures independently.

**Digital Signatures:** Similar to signing a physical document, digital signatures provide cryptographic proof that a specific entity (e.g., an employer or university) issued a VC. This lets relying parties trust the information without contacting the issuer directly. Verification is performed entirely against the signature and the issuer's published public key — no central database or registry lookup is required.

**Hash Functions:** These are one-way mathematical functions that transform data into a unique fingerprint. Any change to the data produces a completely different hash, making tampering easy to detect. Hash functions underpin the integrity of a VC and, in the selective-disclosure model below, allow individual claims to be committed to a credential without exposing their values.

**Decentralized Identifiers (DIDs):** A DID is a globally unique, user-managed identifier for an identity — like a web address that isn't tied to any single platform. The platform creates and resolves DIDs directly, with no on-chain registration step. It issues two methods:

* **`did:web`** — the DID document is self-hosted by the service and served over HTTPS at a public URL (for example, `did:web:issuer.example.com:<uuid>`). Resolving the DID simply means fetching that document.
* **`did:key`** — a deterministic, self-describing identifier whose public key is encoded directly in the DID itself (for example, `did:key:z6Mk...`), so no hosting is required.

The service can also resolve `did:jwk` identifiers. A DID document lists the public keys and verification methods needed to validate the credentials and presentations associated with that identity.

**Verifiable Credentials (VCs):** These are tamper-proof digital documents containing verifiable claims about a subject, such as employment status or educational qualifications. VCs are issued by a trusted entity, stored in the holder's wallet, and presented to verifiers when needed. The platform supports two credential formats: **JWT-VC** (`jwt_vc_json`) and **SD-JWT VC** (`vc+sd-jwt`). Cryptographic proofs demonstrate the validity and authenticity of a credential without the verifier needing to contact the issuer.

**Selective Disclosure (SD-JWT):** With the SD-JWT VC format, a credential can be issued so that the holder reveals only the specific claims a verifier actually needs. When a schema or credential defines a disclosure frame (the set of selectively disclosable claim keys), each of those claims is committed to the credential as a salted hash. At presentation time the holder discloses just the required claims — for example, sharing `department` from an `EmployeeBadge@1.0:sd-jwt` credential while keeping `employee_id` and `full_name` private — and the verifier still validates the issuer's signature over the whole credential. This makes minimal, privacy-preserving disclosure a first-class capability.

Together, these technologies let users control their identity data, share verified credentials minimally and securely, and interact online with greater trust and transparency.


# Roles in the SSI framework

SSI involves three core participants, commonly depicted as the **SSI Triangle**:

* **Issuers:** Trusted entities authorized to issue VCs. This could include universities issuing diplomas, governments providing licenses, or employers verifying employment status, etc.
* **Holders:** Individuals or entities who own their DIDs and VCs stored securely in their digital wallets.
* **Verifiers:** Parties who rely on VCs to confirm an identity claim. This could be an employer verifying work experience for a job application or a bank verifying age for opening an account.

<figure><img src="/files/VvgHI4YQDHkdppfPXsid" alt=""><figcaption></figcaption></figure>

Empeiria’s End-to-End Verifiable Data Infrastructure (EVDI) offers tools for all three key participants of the SSI Triangle:

**Issuer:** Empeiria's integration tools let issuers add credential issuance to their existing data infrastructure with minimal changes.

**Holder:** Empeiria places Holders in control of their data. Empe Wallet is a convenient and secure place to store verifiable credentials, which can be shared privately and securely with Verifiers.

**Verifier:** Using Empeiria’s easily deployable tool, verifiers can quickly check the validity of verifiable credentials, without accessing sensitive information or contacting the issuer. This makes verifying information easier and builds trust between verifiers and holders.

In short, EVDI gives all three participants the tools they need: issuance for Issuers, secure storage and sharing for Holders, and fast validity checks for Verifiers.


# One-click deployment

EVDI One-Click Deployment lets a developer or solutions architect stand up verifiable-credential infrastructure without first dealing with Kubernetes configs, Docker images, decentralized-identity setup, and key management.

The alpha release focuses on **rapid SSI setup**—a complete, fully hosted Issuer and Verifier service (with a cloud wallet server) spun up in minutes. Each deployment exposes credential APIs over standard web protocols: **OpenID4VCI** for issuing credentials and **OpenID4VP** for requesting and verifying presentations. The stack ships preconfigured, so you skip the operational overhead of running your own verifiable-credentials infrastructure.

Under the hood, every deployment manages **self-hosted decentralized identifiers**—`did:web` and `did:key` with Ed25519 signing keys. Your `did:web` documents are served directly by the deployment over HTTPS, so each deployment is fully self-contained. (The EMPE blockchain and the `did:empe` method are separate parts of the Empeiria ecosystem.) Each deployment ships with built-in **credential schema management**, **credential offer issuance** via QR code and deep link, and **verification request** flows—so you can issue and verify **SD-JWT VC** and **JWT-VC-JSON** credentials end to end without writing low-level protocol code.

**On the roadmap**: secure AI agent interactions, so developers can embed verifiable data directly into autonomous agents. This is planned work, not part of today's release.

## Key Features

* **Automated Deployment**: Spin up a complete Issuer and Verifier service in minutes.
* **Open Standards**: Issue and verify credentials via OpenID4VCI and OpenID4VP, in SD-JWT VC and JWT-VC-JSON formats.
* **Self-Hosted Identity**: `did:web` and `did:key` are created and served by the deployment—no chain dependency.
* **Secure Defaults**: Preconfigured Ed25519 signing keys, managed key storage, and sensible privacy defaults out of the box.

## In the operator console

Each deployment ships with an operator console where you manage day-to-day SSI operations. Its sidebar screens are **Issuers**, **Verifiers**, **Holders** (cloud wallets), **DIDs**, and **Schemas**, and each Issuer's detail page includes a **DID Document & Config** viewer. You reach it at the deployment's **Admin GUI URL** and sign in through Keycloak.

<figure><img src="/files/HltkH1Onv51o84fygxFs" alt="Operator console with sidebar"><figcaption><p>The operator console (Issuer Admin) with its sidebar: Issuers, Verifiers, Holders, DIDs, and Schemas</p></figcaption></figure>

### Everything starts from a DID

Every Issuer, Verifier, and Holder in the console is bound to a DID. Create one on the **DIDs** screen — pick the **Method** (`key` or `web`) and click **Create DID** — then select it when you register an Issuer or a Verifier. **Holders** are the cloud wallets you can claim credentials into, which is handy for testing an end-to-end flow without a phone.

<figure><img src="/files/QGAGcB35G3LqgbBAer5o" alt="Create DID form on the DIDs screen"><figcaption><p>The <strong>DIDs</strong> screen with the Create DID form — choose the method, then click Create DID</p></figcaption></figure>

<figure><img src="/files/b1g9wqMDyrTyO7f50QVt" alt="Create Issuer form with Available DIDs"><figcaption><p>The <strong>Issuers</strong> screen with the Create Issuer form — pick one of the Available DIDs</p></figcaption></figure>

<figure><img src="/files/pmiSeIXIiH7DwelLrRfd" alt="Create Holder form on the Holders screen"><figcaption><p>The <strong>Holders</strong> screen with the Create Holder form — each holder is a cloud wallet</p></figcaption></figure>

### Signing in to the console

To sign in, open your deployment on the **One-Click Dashboard** and go to its **Keycloak Config** tab. It shows the credentials you use to log in: **Realm Name**, **Realm Username**, **Initial Password**, **Realm Admin URL**, and **Client ID**. Open the deployment's **Admin GUI URL** and sign in with the **Realm Username** and **Initial Password** shown here.

<figure><img src="/files/bLpE0ORLaZdSE6MZETuz" alt="Keycloak Config tab on the One-Click Dashboard"><figcaption><p>The Keycloak Config tab on the One-Click Dashboard — the credentials for signing in to the operator console (values redacted)</p></figcaption></figure>

**Alpha Release**: This is an early release and new features are added regularly. Sign up at [**oneclick.empe.io**](https://oneclick.empe.io/) and choose the **Sandbox** plan (first 30 days free) from the plan list to start deploying at no cost. Questions? Contact [**evdi@empe.io**](mailto:evdi@empe.io?subject=One-Click%20Deployment).


# Registration

This document explains the process of signing up for an account on the One-Click Deployment platform.

***

## Registration Form

The registration form asks for your details:

1. **E-mail:** Your email address used for communication and logging into the platform.
2. **Phone Number:** Your contact number.
3. **Password:** Create a secure password for your account.
4. **Repeat Password:** Re-enter the password to confirm it matches.
5. **First Name:** Your given name.
6. **Last Name:** Your family name.
7. **Organization Name:** The name of your company or team.

Registration is open — no invitation is required.

<figure><img src="/files/Z39tMtGKFEy6yHhYSrUA" alt="Sign Up form on oneclick.empe.io"><figcaption><p>The Sign Up form on oneclick.empe.io</p></figcaption></figure>

***

## Verification Process

After submitting the registration form, you will receive a verification code at the email address you provided. Enter this code to complete registration.

***

## Terms and Privacy

By signing up, you agree to the platform's **Terms and Conditions** and **Privacy Policy**. Read both before completing your registration.

***

## Final Step

Once all fields are filled out and the terms are accepted:

1. Click the **Sign Up** button to complete your registration.
2. If you already have an account, use the **Log In** link to access it.

***

## Choosing a Plan

After registration, pick a plan from the plan list:

* **Sandbox** — ideal for hackathons and proofs of concept. The first **30 days are free**, then **$29/month**. Click **Start trial** to begin.
* **Pro / Enterprise** — for commercial deployments. Click **Contact us** to get in touch.

Choose **Sandbox** and click **Start trial** to deploy and test your Issuer and Verifier during the free period.

<figure><img src="/files/cLxHvNPNg4HORBYi1LIc" alt="Plan selection with Sandbox and Start trial"><figcaption><p>Plan selection — choose <strong>Sandbox</strong> and click <strong>Start trial</strong></p></figcaption></figure>

For any questions or issues during the registration process, please contact our support team.


# Login

To access your account, follow the steps below:

1. **Access the Login Page:**
   * Open the `One-Click Deployment` application.
   * Navigate to the **Log In** page.
2. **Fill in the Login Form:**
   * Enter the following details:
     * **E-mail:** The e-mail address you used to register.
     * **Password:** Your account password.
3. **Submit the Form:**
   * Click **Log In** to access your account.

After logging in you have an authenticated session and can access your deployments from the dashboard.

***

## Forgot Your Password?

If you cannot remember your password, click the **Forgot Password?** link on the Log In page. Enter your registered e-mail address and the platform sends you a reset link. Follow the link to choose a new password, then return to the Log In page to sign in.

***

Don't have an account yet? Use the **Sign Up** link on the Log In page to create one — see [Registration](/develop/intro/registration) for details.

For any issues during registration or login, please contact our support team for assistance.


# Creating a Deployment

A One-Click deployment provisions a complete SSI stack behind one hosted domain — an **Issuer** API (OpenID4VCI), a **Verifier** API (OpenID4VP), and a **wallet server** for cloud-held credentials.

1. Click **New Deployment** to open the **Create New Deployment** form.
2. Fill in the form fields:
   * **Deployment Name:** A short name for your deployment (visible only to you).
   * **Deployment Domain:** The subdomain for your deployment, served under `evdi.app` — for example, `empe` becomes `empe-issuer.evdi.app`.
   * **Organization Name:** The name of the organization responsible for this deployment.
   * **Version:** Select the version you want to deploy.
   * **Blockchain Configuration:** Select the network for your deployment — for example, **TESTNET**. *Note: Your deployment issues and verifies credentials using self-hosted DIDs and DID documents that the service creates and hosts for you.*

<figure><img src="/files/VEdRkzpGdPVQSNQ7AbPE" alt="Create New Deployment form"><figcaption><p>The Create New Deployment form</p></figcaption></figure>

3. Click **Create Deployment** to complete the process.
4. **Save your Keycloak access credentials.** The portal shows them **once** and does not display them again. They authenticate you to the deployment's identity provider (Keycloak):

   * **Realm Name** — your deployment's Keycloak realm.
   * **Username** and **Initial Password** — the realm admin account.
   * **Realm Admin URL** — the Keycloak admin console for this realm.
   * **Client ID** — the OIDC client your application uses.

   You use these to obtain the OIDC **Bearer token** that your API requests carry (`Authorization: Bearer <token>`) — see [Authenticating to Your Deployment](/develop/intro/authenticating-to-your-deployment). Store them like any admin secret; you can rotate them later from the Keycloak admin console (Realm Admin URL).

<figure><img src="/files/7WQXlhzzENboF8BZipIm" alt="Keycloak access credentials shown after creation"><figcaption><p>The Keycloak access credentials shown once after creation (values redacted here)</p></figcaption></figure>

5. Wait until the deployment status changes to **Active**. Please note that it may take some time to activate. If an error occurs or the status does not change to "Active," please contact support for assistance.
6. Your deployment will appear in the **My Deployments** table once created.

<figure><img src="/files/6gncepEhWSR8MI9mNRXo" alt="My Deployments table with an Active deployment"><figcaption><p>The My Deployments table with a deployment in Active status</p></figcaption></figure>

Once **Active**, the deployment exposes its API documentation and a self-hosted DID document — see the [Issuer Data Description](/develop/intro/issuer_data). From its detail tabs you can manage schemas and create credential offers.


# Issuer Data Description

This document explains the details available in the **Issuer Details** section of the One-Click Deployment dashboard. An Issuer is identified by a **Decentralized Identifier (DID)** that the service creates and hosts itself. Every action described here is performed against the Issuer's API using a valid admin **Bearer (JWT)** token in the `Authorization` header.

***

## Issuer Details

When you select an Issuer from the dashboard, you are taken to a detailed view organized into the following information and tabs.

### General Information

* **id:** The Issuer's identifier, which is its Decentralized Identifier (DID) — for example `did:web:example-name.evdi.app:<uuid>` or `did:key:...`. This is the same DID shown in the full DID document below; the deployment's unique id is the `<uuid>` embedded in the `did:web` path.
* **fullHost:** The base URL where the Issuer service is accessible (for example, `https://example-name.evdi.app`). The Issuer's interactive OpenAPI documentation is served from this host at `/api-docs` (the raw OpenAPI JSON is available at `/api-docs-json`).
* **status:** The current deployment status of the Issuer (for example, `ACTIVE`, `PENDING`, `INIT`, `UPGRADE`, `FAILED`).
* **version:** The deployed version of the Issuer's configuration.
* **networkName:** The deployment-environment selector for this Issuer. Its value comes from `{MAINNET, TESTNET}`; it names the environment the deployment runs in, not an on-chain network the Issuer uses.
* **issuerDomainName:** The subdomain label chosen for the deployment (for example, `example-name-issuer`).
* **issuerName:** The name assigned to the Issuer during creation.
* **swaggerApi:** The URL of the interactive API docs, served from `fullHost` at `/api-docs`.
* **adminGuiUrl:** The URL of the operator console (Issuer Admin) for this deployment.

### Cryptographic Information

* **didDocument:** A JSON object describing the Issuer's DID. For a `did:web` identity, the service hosts this document publicly at `/:uuid/did.json` (also reachable at `/:uuid/.well-known/did.json`), so wallets and verifiers can resolve the Issuer's keys over HTTPS. It includes:
  * **id:** The DID identifier (for example, `did:web:example-name.evdi.app:<uuid>`).
  * **@context:** The JSON-LD contexts for the document, including the DID v1 context and the Ed25519 2018 signature suite context.
  * **verificationMethod:** The Issuer's signing keys. Each entry uses an **Ed25519** key (`type: "Ed25519VerificationKey2018"`), with an **id**, a **controller** (the DID that controls the key), and a **publicKeyBase58** (Base58-encoded Ed25519 public key).

In the operator console, the **DIDs** screen lists every DID the deployment has created and shows the same document under **Show DID Document**.

<figure><img src="/files/ns9sTZBg99dfSl9ZAebn" alt="DIDs screen with a DID document open"><figcaption><p>The <strong>DIDs</strong> screen in the operator console, with a DID's document open</p></figcaption></figure>

***

## Issuer Tabs (Operator Console)

On an Issuer's detail page in the **operator console**, the following tabs are available (the portal's own deployment view exposes separate operational tabs such as Details, Service Accounts, Logs, Backups, Domain Linking, Keycloak Config, Instruction, and Actions):

* **Schemas:** View the schemas assigned to this Issuer, assign published schema versions, and issue credentials from them via each version's **Issue credential** action. Assigning a schema version makes its credential configuration ids (such as `EmployeeBadge@1:sd-jwt`) available to offers. Schemas themselves are defined and versioned on the separate **Schemas** screen.
* **Issuance Sessions:** Track issuance sessions (Session ID, State, Created/Expires). Open a session's dashboard to re-display its offer QR code, copy the `openid-credential-offer://` URL, or claim the credential straight into a cloud wallet. New offers are created from the **Schemas** tab's Issue credential action.
* **DID Document & Config:** The full DID document in JSON, as served from the Issuer's hosted `did.json`, together with the Issuer's OpenID4VCI metadata document (served under `.well-known/did-configuration`), which describes the credentials the Issuer can offer.

High-impact operations are available from the deployment's header on the dashboard rather than as a tab.

<figure><img src="/files/nFW9GpMjh9yOKjF3njov" alt="Issuers list in the operator console"><figcaption><p>The <strong>Issuers</strong> screen in the operator console — open an Issuer's <strong>Dashboard</strong> to reach these tabs</p></figcaption></figure>

***

## Actions

1. **Upgrade Version:**
   * Upgrade the Issuer to a newer version.
2. **Delete:**
   * Permanently remove the Issuer from the system.

To rotate the deployment's admin credentials, use the Keycloak admin console (the Realm Admin URL shown when the deployment was created) — see [Authenticating to Your Deployment](/develop/intro/authenticating-to-your-deployment).

***

## Example View

Here is an example of an Issuer's details as displayed on the dashboard:

```plaintext
issuerId: did:web:example-name.evdi.app:646941a4-1d89-60e7-d66e-6afddccb7a68
issuerName: example-name-issuer
fullHost: https://example-name.evdi.app
swaggerApi: https://example-name.evdi.app/api-docs
status: ACTIVE
version: 0.0.4
networkName: TESTNET

didDocument:
{
  "id": "did:web:example-name.evdi.app:646941a4-1d89-60e7-d66e-6afddccb7a68",
  "@context": [
    "https://www.w3.org/ns/did/v1",
    "https://w3id.org/security/suites/ed25519-2018/v1"
  ],
  "verificationMethod": [
    {
      "id": "did:web:example-name.evdi.app:646941a4-1d89-60e7-d66e-6afddccb7a68#key-1",
      "type": "Ed25519VerificationKey2018",
      "controller": "did:web:example-name.evdi.app:646941a4-1d89-60e7-d66e-6afddccb7a68",
      "publicKeyBase58": "<base58-encoded-public-key>"
    }
  ],
  "authentication": [
    "did:web:example-name.evdi.app:646941a4-1d89-60e7-d66e-6afddccb7a68#key-1"
  ],
  "assertionMethod": [
    "did:web:example-name.evdi.app:646941a4-1d89-60e7-d66e-6afddccb7a68#key-1"
  ]
}
```

<figure><img src="/files/q0x6fzdbTPKOeTEhFcRz" alt="Issuer Details in the One-Click portal"><figcaption><p>Deployment details in the One-Click portal (Issuer Details): Full Host, Swagger Api, Admin Gui Url, and status</p></figcaption></figure>


# Creating a Verifier

A Verifier is created inside your deployment's **operator console** — not as a separate portal deployment. It runs on the same service (and uses the same Keycloak login) as the Issuer; you register a Verifier by binding it to a DID.

1. In the operator console sidebar, open **Verifiers**.
2. In **Available DIDs**, select the DID the Verifier will use to sign authorization requests. If no DID is available yet, open **DIDs** first and create one (a `did:key` or `did:web`).
3. Click **Create Verifier**.
4. Your Verifier appears in the **Verifiers** list, identified by its DID. Open its **Dashboard** to view the Verifier's details (its Verifier ID).

<figure><img src="/files/p9HGXluwOj9gIzfHC3TW" alt="Verifiers screen in the operator console"><figcaption><p>The <strong>Verifiers</strong> screen before any Verifier is registered</p></figcaption></figure>

<figure><img src="/files/WHSWsYdgXeh0yAYfj8D8" alt="Create Verifier form with Available DIDs"><figcaption><p>The Create Verifier form — select the DID the Verifier will sign authorization requests with</p></figcaption></figure>

<figure><img src="/files/SthFzrEiagQayuYWejW1" alt="Verifier details with the Verifications tab"><figcaption><p>Verifier details — the Verifier ID and the <strong>Verifications</strong> tab</p></figcaption></figure>

Authorization (presentation) requests are created — and verification sessions tracked — from the Verifier's **Verifications** tab, or through the Verifier API: `POST /verifiers/{verifierId}/authorization-requests` (with `/qr-code`) and `GET /verification-sessions/{id}`. The Verifier shares requests as QR codes or deep links, verifies presented **SD-JWT VC** and **JWT-VC-JSON** credentials, and uses the same admin **Bearer token** as the rest of the deployment — see [Verifying Credentials](/develop/intro/verifying-credentials), the [Verifier API](/develop/verifier), and [Authenticating to Your Deployment](/develop/intro/authenticating-to-your-deployment).

You can also create a Verifier programmatically with `POST /agent/verifier` — see the [Verifier API](/develop/verifier).


# Verifier Data Description

A **Verifier** requests and validates credential presentations. You create one in the operator console (see [Creating a Verifier](/develop/intro/create_verifier)); it is identified by a DID it uses to sign requests. This page describes the data that identifies a Verifier and how to reach its API.

A Verifier builds **OpenID4VP** authorization (presentation) requests — shared as a QR code or an authorization-request URI — tracks the resulting verification sessions, and validates presented **SD-JWT VC** and **JWT-VC-JSON** credentials, including selective disclosure.

***

## Identity

* **verifierId:** The Verifier's identifier, used in every authorization request. In the console it is the **DID** (`did:web:...` or `did:key:...`) you select when creating the Verifier, and that DID becomes the default key for signing the OpenID4VP authorization requests it sends to holders. Via the raw API the verifierId may instead be any identifier (the service generates one if you omit it); when it is not a DID, supply a `signingDid` in each authorization request so the Verifier has a key to sign with.
* **clientMetadata** *(optional)*: OpenID4VP client metadata attached to the Verifier.

## Where the Verifier API Lives

The Verifier API is served at your deployment's base URL (`fullHost`), with interactive docs at `/api-docs` (raw OpenAPI at `/api-docs-json`). In the current One-Click setup a single deployment hosts both the Issuer and Verifier APIs, so this is the same base URL as the Issuer's; in other setups the Verifier may run under its own host. Administrative calls use the same admin **Bearer token** as the rest of the deployment — see [Authenticating to Your Deployment](/develop/intro/authenticating-to-your-deployment).

***

## Cryptographic Identity

The Verifier has its own self-hosted **Decentralized Identifier (DID)** — a `did:web` or `did:key` using Ed25519 keys — which it uses to sign the OpenID4VP authorization requests it sends to holders. For a `did:web` Verifier, the corresponding DID document is served over HTTPS at `/:uuid/did.json` (also reachable at `/:uuid/.well-known/did.json`), so holders can resolve it and verify the request signature.

* **didDocument:** A JSON object describing the Verifier's DID. It includes:
  * **id:** The DID identifier (for example, `did:web:example-name.evdi.app:<uuid>`).
  * **@context:** The JSON-LD contexts for the document, including the DID v1 context and the Ed25519 2018 signature suite context.
  * **verificationMethod:** The Verifier's signing keys. Each entry uses an **Ed25519** key (`type: "Ed25519VerificationKey2018"`), with an **id**, a **controller** (the DID that controls the key), and a **publicKeyBase58** value (the Base58-encoded Ed25519 public key).

***

## Example

Creating a Verifier (`POST /agent/verifier`) returns its identifier:

```json
{
  "verifierId": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
}
```

For a `did:web` Verifier, the corresponding DID document is available at its hosted `did.json`:

```json
{
  "id": "did:web:example-name.evdi.app:646941a4-1d89-60e7-d66e-6afddccb7a68",
  "@context": [
    "https://www.w3.org/ns/did/v1",
    "https://w3id.org/security/suites/ed25519-2018/v1"
  ],
  "verificationMethod": [
    {
      "id": "did:web:example-name.evdi.app:646941a4-1d89-60e7-d66e-6afddccb7a68#key-1",
      "type": "Ed25519VerificationKey2018",
      "controller": "did:web:example-name.evdi.app:646941a4-1d89-60e7-d66e-6afddccb7a68",
      "publicKeyBase58": "<base58-encoded-public-key>"
    }
  ],
  "authentication": [
    "did:web:example-name.evdi.app:646941a4-1d89-60e7-d66e-6afddccb7a68#key-1"
  ],
  "assertionMethod": [
    "did:web:example-name.evdi.app:646941a4-1d89-60e7-d66e-6afddccb7a68#key-1"
  ]
}
```


# Managing Credential Schemas

This document explains how to define and manage **credential schemas** from the **Schemas** screen of the One-Click Deployment operator panel. A schema describes the claims a credential carries, the formats it can be issued in, and the metadata wallets use to display it. Once defined, a schema can be assigned to any of your Issuers and referenced when creating credential offers.

<figure><img src="/files/f8uaU1aRnMZJCvJjcX6K" alt="Schemas screen with the schema list"><figcaption><p>The Schemas screen with the schema list</p></figcaption></figure>

***

## Opening the Schemas Screen

1. Open the operator panel and click **Schemas** in the sidebar menu.
2. The screen lists every schema you have created, grouped by schema type (the VCT identifier, or the name when no VCT is set) and showing the **active** version of each.
3. Click an existing schema to view or version it, or click **Create Schema** to define one from scratch.

***

## Defining a Schema

The schema builder authors a schema through an **attribute builder** form. Its columns are **Attributes**, **JSON**, **AI**, and **History**. You add each claim as an attribute using form fields, and the panel keeps an equivalent **JSON** view in sync (a Monaco-based editor with syntax highlighting, bracket matching, and live validation) that you can also edit directly. As you work, the panel validates the claim properties and flags errors inline before you can publish.

A schema is described by the following fields:

* **name:** The schema name, for example `EmployeeBadge`. Use a name that starts with a letter or digit and contains only letters, digits, underscores, and hyphens. The Define form itself also accepts spaces (and rejects dots), but the create-schema API the panel calls on publish enforces `^[A-Za-z0-9][A-Za-z0-9._-]*$`, so names containing spaces fail at publish.
* **version:** The schema version. The panel assigns it automatically — a new schema starts at version `1`, and each new version increments it.
* **type:** The Verifiable Credential Type (VCT) identifier, used for SD-JWT credentials, for example `EmployeeBadgeCredential`. The panel restricts this field to letters, digits, underscores, and hyphens; to publish a URL-style VCT such as `https://issuer.example.com/vct/employee-badge`, call `POST /schemas` directly with the `vct` field.
* **description:** A human-readable description of the credential. Required in the panel's Define form; optional when calling the create-schema API directly.
* **attributes (claims):** Each claim has a **name**, a **type**, an optional **title** and **description**, and a **Required** flag. Together the attributes form the JSON-Schema definition of the claims (`type`, `properties`, and `required`).

To define the schema:

1. Click **Create Schema**, then enter the schema **name**, **description**, and **type** and click **Define New Schema** to open the schema builder.
2. Add each claim using the attribute builder: set its **name** and **type**, optionally a **title** and **description**, and tick **Required** for mandatory claims. The panel keeps the equivalent JSON view in sync under the **JSON** column, which you can also edit directly.
3. Click **Publish** to save the schema. It becomes the active version under its name.

<figure><img src="/files/TGSINereat0doiy8i4ec" alt="Create Schema form with name, description and type"><figcaption><p>The Create Schema form — the schema's name, description, and type, before opening the builder</p></figcaption></figure>

The panel publishes the schema as `{ name, version, description, vct, schema }`. A published schema defaults to both credential **formats**, `sd-jwt` and `jwt-vc-json`; the panel does not offer a format field. The `disclosureFrame` (SD-JWT selective-disclosure keys) is likewise an option on the underlying create-schema API request rather than a field in the Schemas screen.

<figure><img src="/files/uaQyZcJqo02jKZIloWS4" alt="Schema builder with Attributes and JSON"><figcaption><p>The schema builder — the Attributes list and live JSON view</p></figcaption></figure>

### Example Schema Definition

The attributes you add in the builder produce a schema definition like the one below. This illustrates the resulting create-schema request rather than something you type wholesale into a single editor. The `formats` and `disclosureFrame` fields are optional API-level options that the Schemas screen does not collect.

```plaintext
{
  "name": "EmployeeBadge",
  "version": "1",
  "description": "Employee access badge credential.",
  "formats": ["sd-jwt", "jwt-vc-json"],
  "schema": {
    "type": "object",
    "properties": {
      "employee_id": { "type": "string" },
      "full_name":   { "type": "string" },
      "department":  { "type": "string" }
    },
    "required": ["employee_id", "full_name", "department"]
  },
  "vct": "EmployeeBadgeCredential",
  "disclosureFrame": ["employee_id", "full_name", "department"]
}
```

***

## Versioning a Schema

Schemas are versioned by name. Each `name@version` pair is unique, so you cannot overwrite an existing version, but you can publish new ones alongside it.

1. Open an existing schema and click **Add New Version** to start a new version from its definition.
2. Adjust the claims in the attribute builder (or the synced **JSON** view). The panel assigns the next version number automatically (for example `1` → `2`); the version is not editable in the builder. To publish an arbitrary version string such as `1.1`, call `POST /schemas` directly.
3. Click **Publish**.

When you publish a new version, it becomes the **active** version for that name and the previous version is marked **superseded**. Superseded versions are retained and can still be viewed in the schema's version history; the schema list shows the active version by default.

<figure><img src="/files/f8uaU1aRnMZJCvJjcX6K" alt="Schema version panel"><figcaption><p>The schema's version panel, with the published version and Add New Version</p></figcaption></figure>

***

## Credential Configuration IDs

A schema combined with a format produces a **credential configuration id** of the form `Name@Version:format`. For the example above this yields:

```plaintext
EmployeeBadge@1:sd-jwt
EmployeeBadge@1:jwt-vc-json
```

These identifiers are what credential offers reference when issuing credentials, so a single schema published in two formats exposes two configuration ids.

***

## Assigning a Schema to an Issuer

Before an Issuer can offer a credential, the schema must be assigned to it.

1. Go to the Issuer's detail page and open the **Schemas** tab.
2. Select the schema to assign, identified by its `Name@Version`, for example `EmployeeBadge@1`.
3. Confirm the assignment.

The schema is added to the Issuer and its OpenID4VCI issuer metadata is refreshed so the credential configuration ids become available for offers. The Issuer can now issue credentials for any configuration id derived from that schema, such as `EmployeeBadge@1:sd-jwt`.

<figure><img src="/files/dTnfIzfuvbILeSpwWR8C" alt="Issuer detail Schemas tab with no schemas assigned"><figcaption><p>The Issuer's <strong>Schemas</strong> tab before anything is assigned — start from <strong>Assign schema</strong></p></figcaption></figure>

<figure><img src="/files/qsACBxBMJr2tG3wJyPgh" alt="Assign schema to issuer dialog"><figcaption><p>The Assign schema to issuer dialog</p></figcaption></figure>

***

For any issues while defining, versioning, or assigning schemas, please contact our support team for assistance.


# Issuing Credentials

This guide explains how to issue a verifiable credential from the operator panel: pick an assigned schema, fill in the claim values, create a **credential offer**, and hand it to a holder as a QR code or deep link—or claim it directly into one of your cloud wallets.

Every credential offer is an **OpenID4VCI** pre-authorized offer. The offer is delivered as an `openid-credential-offer://...` URI (rendered as a QR code), which a compatible wallet scans to collect the credential.

***

## Open the Issue form

1. From the **Sidebar Menu**, open **Issuers** and select the Issuer you want to issue from.
2. On the Issuer detail page, open the **Schemas** tab. The **Assigned schemas** section lists every schema bound to this Issuer, grouped by name and credential type, with one row per version.
3. Find the schema version you want and click **Issue credential**. *Note: the button is enabled only for **Published** schema versions; draft versions cannot be issued.*

<figure><img src="/files/UCTze8QTHlM2hdU3vtO8" alt="Issuer detail Schemas tab with Issue credential button"><figcaption><p>Issuer detail — the Schemas tab with an assigned schema and the Issue credential button</p></figcaption></figure>

***

## Fill in the claim fields

The **Issue Credential** form is generated automatically from the schema's claim definitions. Each property becomes an input, required fields are marked, and field titles and descriptions come from the schema.

1. Enter a value for every required claim (for example **employee\_id**, **full\_name**, **department**). Optional claims left blank are omitted from the credential.
2. Click **Generate offer** to create the offer.

<figure><img src="/files/6zApUV46zK7sjv40nu6Y" alt="Issue Credential form with claim inputs"><figcaption><p>The Issue Credential form with claim inputs filled in</p></figcaption></figure>

***

## Share the credential offer

After the offer is created, a **QR Code** panel appears next to the form with the offer rendered as a scannable image. From this panel you can:

* **Scan** the QR code with a wallet to collect the credential.
* **Download** the QR code as a PNG.
* **Print** the QR code.
* **Copy offer URL** to copy the raw `openid-credential-offer://...` URI for sharing through another channel.

<figure><img src="/files/6kaMLsgxIHlHUmsazTgv" alt="QR Code panel with Download, Print, Copy offer URL"><figcaption><p>The QR Code panel with Download, Print, and Copy offer URL</p></figcaption></figure>

### What gets sent

Issuing from the form creates the offer via the credential-offers endpoint of the selected Issuer.

**Endpoint**

`POST /issuers/{issuerDid}/credential-offers/qr-code`

**Request Body**

```json
{
  "credentialConfigurationIds": ["EmployeeBadge@1:sd-jwt"],
  "issuanceMetadata": {
    "claimsByConfigurationId": {
      "EmployeeBadge@1:sd-jwt": {
        "employee_id": "E-1024",
        "full_name": "Ada Lovelace",
        "department": "Engineering"
      }
    }
  }
}
```

* **credentialConfigurationIds:** the credential configuration to issue, in the form `Name@Version:format` (e.g. `EmployeeBadge@1:sd-jwt`). The panel resolves this from the schema version you selected.
* **issuanceMetadata.claimsByConfigurationId:** the claim values you entered, keyed by the same configuration id.

The `qr-code` endpoint returns a **PNG** image of the QR code, with the offer URI in the `X-Credential-Offer-Uri` response header (and `X-Pin` when a PIN is required). The plain-JSON variant, `POST /issuers/{issuerDid}/credential-offers`, returns the offer details directly:

**Response Body**

```json
{
  "credentialOfferUri": "openid-credential-offer://?credential_offer=...",
  "issuanceSessionId": "a1b2c3d4-...",
  "credentialConfigurationIds": ["EmployeeBadge@1:sd-jwt"],
  "userPin": "1234"
}
```

* **credentialOfferUri:** the `openid-credential-offer://...` URI behind the QR code.
* **issuanceSessionId:** the id of the issuance session created for this offer; track its progress under the Issuer's **Issuance Sessions** tab.
* **userPin:** present only when the offer requires a PIN (see below).

***

## Track issuance sessions

Every offer creates an issuance session, listed under the Issuer's **Issuance Sessions** tab as a table of Session ID, State, and Created/Expires timestamps. Opening a session's **Session Dashboard** side panel re-displays the offer QR code for an active session, offers **Copy offer URL**, lists the issued credentials of a completed session, and lets you claim the offer straight into a cloud wallet.

***

## SD-JWT VC and JWT-VC-JSON

A schema can be published in two credential formats, and each format produces its own credential configuration id:

* **SD-JWT VC** — configuration id ends in `:sd-jwt` (e.g. `EmployeeBadge@1:sd-jwt`). SD-JWT credentials support **selective disclosure**, so the holder can later reveal individual claims to a verifier instead of the whole credential. The claims a schema marks as selectively disclosable are defined when the schema is created.
* **JWT-VC-JSON** — configuration id ends in `:jwt-vc-json` (e.g. `EmployeeBadge@1:jwt-vc-json`). The credential is a single signed JWT with all claims always present.

Pick the configuration that matches how the credential will be used: choose **SD-JWT VC** when holders need to share claims minimally, or **JWT-VC-JSON** for a simpler all-or-nothing credential.

The Issue Credential form resolves the credential configuration id automatically from the schema version you selected and issues that configuration; for a schema published in both formats, the panel picks one configuration for you. To issue a specific format, call `POST /issuers/{issuerDid}/credential-offers` with the exact `credentialConfigurationIds` you want (for example `EmployeeBadge@1:jwt-vc-json`).

***

## PIN-protected offers

A credential offer can require the holder to enter a short transaction PIN before the credential is released. This binds collection to someone who has both the QR code and the PIN, which you can deliver over a separate channel.

When an offer requires a PIN, the offer is created with `requirePin: true`, the response includes a `userPin` value, and the QR-code response carries the PIN in the `X-Pin` header. Share that PIN with the holder out of band.

PIN-protected offers are created through the API: send `requirePin: true` in the body of `POST /issuers/{issuerDid}/credential-offers` (or its `/qr-code` variant). The Issue Credential form always creates offers without a PIN.

***

## Claim into a cloud wallet

Instead of handing the QR code to an external wallet, you can claim the offer directly into one of your own **cloud wallet** holders—useful for testing an end-to-end flow.

1. In the **QR Code** panel, find the **Claim with wallet** section.
2. Select a holder from the **Wallet** dropdown (the list shows the holders you have created).
3. Click **Claim**.

The panel sends the offer URI to the selected holder, which collects the credential into its wallet. On success you are taken to that holder's **Credentials** view, where the new credential appears.

<figure><img src="/files/6kaMLsgxIHlHUmsazTgv" alt="Claim with wallet section"><figcaption><p>The Claim with wallet section with the holder dropdown and Claim button</p></figcaption></figure>

<figure><img src="/files/URsVYG6S0BEzXmODRWUO" alt="Holder details Credentials tab with the claimed credential"><figcaption><p>The holder's <strong>Credentials</strong> view after a successful claim, with the credential's raw JSON</p></figcaption></figure>

**Endpoint**

`POST /holders/{holderId}/process`

**Request Body**

```json
{
  "uri": "openid-credential-offer://?credential_offer=..."
}
```

**Response Body**

```json
{
  "status": "success",
  "claimed": 1,
  "ids": ["..."]
}
```

If you need to claim into a holder that does not yet exist, create one first from the **Holders** section, then return to the offer and select it in the **Wallet** dropdown.

<figure><img src="/files/B8cQKN1obWq7PWA5Y40s" alt="Holders list with Dashboard and Claim credential actions"><figcaption><p>The <strong>Holders</strong> screen — each holder offers a <strong>Dashboard</strong> and a <strong>Claim credential</strong> action</p></figcaption></figure>


# Verifying Credentials

This page walks you through verifying credentials with a deployed **Verifier**: creating an OpenID4VP presentation request (as a URI or a scannable QR code), tracking the verification session live, and verifying an SD-JWT or JWT-VC credential directly.

Every request below targets your Verifier deployment's base URL (for example, `https://example-name-verifier.evdi.app`) and must include a valid Bearer token in the `Authorization` header. In local development you can set `AUTH_DISABLED=true` to skip authentication. The full, interactive API reference is always available at `/api-docs`.

***

## Creating a Request from the Console

You can create a presentation request without writing any code. On the Verifier's detail page, open the **Verifications** tab and click **Create verification**.

1. Choose **From credential schema** to pick a published schema and tick the claims to request, or **Custom** to specify the credential type and claim paths by hand.
2. Optionally set an **Issuer DID**. When set, only credentials issued by that DID are accepted (matched on the SD-JWT `iss` claim); leave it empty to accept any issuer.
3. Click **Create verification**.

<figure><img src="/files/C5hddIxHA6riavau7yYK" alt="Create verification form"><figcaption><p>The Create verification form — pick a published schema and the claims to request</p></figcaption></figure>

The console renders the request as a QR code alongside its **Session ID** and **Request URI**, and tracks the session live while it waits for a wallet to scan.

<figure><img src="/files/0NQkhL4668a3Lvp36TUc" alt="Verification request QR code waiting for a wallet"><figcaption><p>The verification request as a QR code, with the session waiting for the wallet to scan</p></figcaption></figure>

The rest of this page covers the same flow through the API.

***

## Creating a Presentation Request

A presentation request asks a wallet to present one or more credentials. Create one against the Verifier you deployed earlier.

1. Pick the Verifier you want to use and note its identifier (`verifierId`). This is the value shown on the Verifier detail page.
2. Decide **which credentials to request**. Provide **exactly one** of:
   * `dcqlQuery` — a DCQL query, or
   * `presentationDefinition` — a DIF Presentation Exchange v2 definition.
3. Send the request to **`POST /verifiers/{verifierId}/authorization-requests`**.
4. Share the returned `authorizationRequestUri` with the wallet, or use the QR-code endpoint described below.

### Request Body

* **dcqlQuery** *(object, optional)*: A DCQL query describing the requested credentials. Required if `presentationDefinition` is not provided.
* **presentationDefinition** *(object, optional)*: A DIF Presentation Exchange v2 definition. Required if `dcqlQuery` is not provided.
* **responseMode** *(string, optional)*: One of `direct_post`, `direct_post.jwt`, `dc_api`, `dc_api.jwt`. Defaults to `direct_post.jwt`.
* **version** *(string, optional)*: OpenID4VP version to use — `v1`, `v1.draft21`, or `v1.draft24`. Defaults to `v1`.
* **expectedOrigins** *(string\[], optional)*: Expected origins for Digital Credentials API flows.
* **authorizationResponseRedirectUri** *(string, optional)*: Redirect URI to include in the authorization response.
* **signingDid** *(string, optional)*: DID that signs the request. If omitted, the `verifierId` path parameter must already be a DID.

> **Note:** `presentationDefinition` requires `"version": "v1.draft24"` (or `v1.draft21`); the default `v1` supports only `dcqlQuery`.

Example body requesting an `EmployeeBadge@1:sd-jwt` credential via DCQL:

```json
{
  "dcqlQuery": {
    "credentials": [
      {
        "id": "employee_badge",
        "format": "dc+sd-jwt",
        "meta": { "vct_values": ["https://example-name.evdi.app/vct/employee-badge"] },
        "claims": [
          { "path": ["full_name"] },
          { "path": ["department"] }
        ]
      }
    ]
  },
  "responseMode": "direct_post.jwt",
  "version": "v1"
}
```

### Response Body

* **authorizationRequestUri** *(string)*: The `openid4vp://` request URI to hand to a wallet.
* **verificationSessionId** *(string)*: The session identifier used to track this verification.
* **authorizationRequestId** *(string, optional)*: Identifier of the hosted request, when applicable.
* **expiresAt** *(string, optional)*: ISO timestamp after which the request is no longer valid.

```json
{
  "authorizationRequestUri": "openid4vp://?request_uri=...",
  "verificationSessionId": "18b760b1-4ab8-4d38-9d42-6b4c25a4d2e0",
  "authorizationRequestId": "1b8a6a8b-2ac1-4f0b-9d9c-6b9c859b1af2",
  "expiresAt": "2024-08-09T12:34:56.789Z"
}
```

***

## Showing the Request as a QR Code

To present the request to a mobile wallet, request a ready-made QR image instead of building one yourself.

1. Send the **same body** as above to **`POST /verifiers/{verifierId}/authorization-requests/qr-code`**.
2. The response is a **PNG image** of the QR code, encoding the `openid4vp://` request URI.
3. Read the accompanying response headers to keep tracking the session:
   * **X-Authorization-Request-Uri** — the same request URI encoded in the image.
   * **X-Verification-Session-Id** — the session id to poll or stream.
   * **X-Authorization-Request-Id** — present for hosted requests.

Display the image to the user, who scans it with their wallet to begin presenting their credentials.

***

## Tracking the Verification Session

After the request is created, follow its progress in real time as the wallet responds.

### Read the current state

Call **`GET /verification-sessions/{sessionId}`** to fetch a snapshot of the session.

* **id** *(string)*: Internal session identifier.
* **verifierId** *(string)*: The Verifier that created the session.
* **state** *(string)*: Current lifecycle state, for example `RequestCreated`.
* **authorizationRequestUri** *(string, optional)*: The request URI for this session.
* **authorizationRequestId** *(string, optional)*: Hosted request identifier, when applicable.
* **authorizationResponseRedirectUri** *(string, optional)*: Redirect URI supplied during request creation.
* **expiresAt** *(string, optional)*: ISO expiry timestamp.
* **errorMessage** *(string, optional)*: The last error observed in the session lifecycle.

```json
{
  "id": "18b760b1-4ab8-4d38-9d42-6b4c25a4d2e0",
  "verifierId": "did:web:example-name-verifier.evdi.app:646941a4-1d89-60e7-d66e-6afddccb7a68",
  "state": "RequestCreated",
  "authorizationRequestUri": "openid4vp://?request_uri=...",
  "authorizationRequestId": "1b8a6a8b-2ac1-4f0b-9d9c-6b9c859b1af2",
  "authorizationResponseRedirectUri": "https://your-app.example.com/callback",
  "expiresAt": "2024-08-09T12:34:56.789Z"
}
```

### Stream live updates

For a live view, subscribe to **`GET /verification-sessions/{sessionId}/events`**, a Server-Sent Events (SSE) stream.

* The stream emits a **`session`** event each time the session changes; its payload is the session object shown above.
* A **`ping`** heartbeat is emitted every 15 seconds to keep the connection alive.

Watch the `state` field move through the lifecycle until the wallet's presentation has been received and checked. If verification fails, the `errorMessage` field describes what went wrong.

***

## Verifying a Credential Directly

You can also verify a single credential on its own, without running a presentation request — useful when a credential string has already been collected. SD-JWT VC and JWT-VC are both supported; the format is detected automatically.

1. Send the compact credential string to **`POST /credentials/verify`**.
2. Inspect `isValid` in the response. When valid, the decoded contents are returned for inspection.

### Request Body

* **credential** *(string)*: The compact credential to verify, in JWT or SD-JWT form.

```json
{
  "credential": "eyJhbGciOiJFZERTQSJ9.eyJ2Y3QiOi...~WyJhIiwiZnVsbF9uYW1lIl0~"
}
```

### Response Body

* **isValid** *(boolean)*: Whether the credential passed verification.
* **format** *(string, optional)*: Detected format — `dc+sd-jwt` for SD-JWT credentials or `jwt_vc` for JWT-VC credentials.
* **payload** *(object, optional)*: The decoded credential payload.
* **prettyClaims** *(object, optional)*: For SD-JWT, all disclosed claims merged into a single object.
* **error** *(string, optional)*: The reason the credential was rejected, present only when `isValid` is `false`.

```json
{
  "isValid": true,
  "format": "dc+sd-jwt",
  "payload": { "vct": "https://example-name.evdi.app/vct/employee-badge", "iss": "did:web:example-name.evdi.app:..." },
  "prettyClaims": {
    "employee_id": "E-10293",
    "full_name": "Jane Doe",
    "department": "Engineering"
  }
}
```

When a credential cannot be verified, `isValid` is `false` and `error` explains why:

```json
{
  "isValid": false,
  "error": "Invalid signature."
}
```


# Authenticating to Your Deployment

Every One-Click Deployment ships with its own credential API — an **Issuer** API (OpenID4VCI) and a **Verifier** API (OpenID4VP) reachable at your deployment's base URL, for example `https://example-name.evdi.app`. Administrative operations on that API are protected, so you authenticate each request with an admin **Bearer (JWT)** token issued by your deployment's identity provider (Keycloak).

When you create a deployment, the portal shows your **Keycloak access credentials** once — the realm name, admin username and initial password, realm admin URL, and client ID (see [Creating a Deployment](/develop/intro/create_issuer)). You use these to obtain the Bearer token, then present it in the standard HTTP `Authorization` header; the deployment validates it before running the operation. Without a valid token, administrative calls are rejected.

***

## Obtaining a Token

Request an access token from your deployment's Keycloak realm using the account credentials shown when you created the deployment. The token endpoint follows the standard Keycloak path:

```bash
curl -X POST \
  https://keycloak.evdi.app/realms/<REALM_NAME>/protocol/openid-connect/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=password" \
  -d "client_id=<CLIENT_ID>" \
  -d "username=<USERNAME>" \
  -d "password=<PASSWORD>"
```

Fill in the **Realm Name**, **Client ID**, **Username**, and **Initial Password** from the credentials screen, and use the Keycloak host from your **Realm Admin URL**. The response's `access_token` is the Bearer token you send to the deployment's API — it carries the `admin` role the API requires, and it expires, so request a fresh one when it lapses.

***

## How Authentication Works

* The deployment validates a signed **OIDC JWT** on every protected request. You pass the token as a **Bearer** credential in the `Authorization` header.
* The token's signature is checked against the platform's identity provider. A valid token authorizes the credential operations on your deployment — managing schemas and credential offers, signing credentials, and creating authorization requests — and the `admin` role additionally gates the agent/DID and wallet-management endpoints.
* There is no separate static API key or shared password on the API itself — the Bearer token obtained from your deployment's Keycloak is the only credential your client sends with each request.

***

## Using the Token

Send the token in the `Authorization` header on every administrative request, prefixed with `Bearer`:

```http
Authorization: Bearer <YOUR_ACCESS_TOKEN>
```

### Example: Direct API Access

The example below assigns a credential schema to an Issuer by calling the deployed Issuer API directly. Replace the base URL with your deployment's `fullHost`, the path's `:issuerDid` with your Issuer's DID, and `<YOUR_ACCESS_TOKEN>` with the token you obtained above.

```bash
curl -X POST \
  https://example-name.evdi.app/issuers/:issuerDid/schemas \
  -H "Authorization: Bearer <YOUR_ACCESS_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{ "schemaId": "EmployeeBadge@1" }'
```

A successful response confirms the assignment:

```json
{ "status": "assigned" }
```

The same `Authorization: Bearer <token>` header applies to every administrative endpoint — creating credential offers (for example, referencing the `EmployeeBadge@1:sd-jwt` configuration), signing credentials, and creating verification requests. The full set of paths, request bodies, and responses is documented in each deployment's interactive API reference, served at **`/api-docs`** (for example `https://example-name.evdi.app/api-docs`).

***

## Rotating Credentials

If your credentials are ever exposed, or you want to rotate them on a schedule, sign in to your deployment's **Keycloak admin console** (the Realm Admin URL shown when you created the deployment) and reset the admin user's password there (the deployment uses a public Keycloak client, so there is no client secret to rotate).

After rotation, update every client, script, and integration to obtain and send a token minted with the new credentials right away.

***

## Security Notes

* **Treat the token as a secret.** Anyone holding it can perform administrative operations against your deployment. Store it in a secrets manager or environment variable — never commit it to source control or paste it into client-side code.
* **Always use HTTPS.** Bearer tokens must only travel over TLS so the credential is never sent in clear text. Production deployments are served over HTTPS.
* **Send it server-to-server.** Keep the token on your backend and call the deployment API from there; do not embed it in browsers, mobile apps, or other untrusted clients.
* **Rotate on exposure.** If the credentials may have leaked, reset the admin user's password immediately in the Keycloak admin console (see above).

***

## Next Steps

* [**Issuer API**](/develop/issuer) — issue credentials with OpenID4VCI: schemas, credential offers, issuance sessions, and direct signing.
* [**Verifier API**](/develop/verifier) — request and verify presentations with OpenID4VP: authorization requests, verification sessions, and direct verification.

Each deployment also exposes its own live API reference at `/api-docs`, where you can browse and try the exact endpoints your token unlocks.


# Issuer

The Issuer Service is a foundational component of the Self-Sovereign Identity (SSI) ecosystem, enabling the creation, issuance, and management of Verifiable Credentials (VCs). A single service instance can register multiple issuers, each bound to its own DID; issuers define credential schemas, sign credentials, and hand them to interoperable wallets using the **OpenID4VCI** protocol over a standards-based HTTP API.

**Key Highlights:**

* **Standards-Based Verifiable Credentials** Issues W3C-aligned credentials in two formats: **SD-JWT VC** (`vc+sd-jwt` / `sd-jwt-vc`), which supports selective disclosure so holders reveal only the claims a verifier needs, and **JWT-VC-JSON** (`jwt-vc-json` / `jwt_vc_json`). Credentials are cryptographically signed with Ed25519 (EdDSA) keys held in the service's internal key-management system.
* **Schema Management and Versioning** Credential schemas are JSON-Schema templates that define the claims a credential carries, ensuring consistent credential structures. Each schema has a name (e.g. `EmployeeBadge`) and a string version (e.g. `1.0`), and a schema together with a format yields a **credential configuration id** of the form `Name@Version:format` (e.g. `EmployeeBadge@1.0:sd-jwt`). New versions can be created without disturbing existing ones, and a schema is assigned to an issuer before it can be offered.
* **OpenID4VCI Credential Offerings** An issuer creates a **credential offer** for one or more credential configurations. The offer is returned as an `openid-credential-offer://` URI and can be rendered as a QR code for the recipient to scan. Offers are claimed by a wallet through the OpenID4VCI **pre-authorized code flow**, with an optional PIN (transaction code) that the recipient must enter to bind the claim to a person who has the PIN.
* **Secure, Standards-Based Wallet Interactions** Wallets retrieve credentials using the OpenID4VCI protocol against the issuer's published metadata, exchanging the pre-authorized code (and PIN, if required) for the signed credential. Every issuance is tracked as an **issuance session** so operators can observe its state.
* **Direct Credential Signing** Alongside the wallet-based flow, the service exposes a direct signing endpoint that returns a signed credential in compact serialized form without a wallet round-trip — useful for server-to-server integrations and testing.
* **Architecture Overview**
  * **Issuer Service Instance** Runs as a standalone server backed by PostgreSQL. Each issuer is registered against an existing DID and exposes its own OpenID4VCI issuer metadata. Issuers share the instance's schema store and database: a schema assigned to one issuer becomes available as credential configurations built from all active schemas.
  * **Decentralized Identifiers** The service creates and serves **`did:web`** documents itself over HTTPS (resolvable at `/:uuid/did.json`) and **`did:key`** identifiers, using Ed25519 keys (see [Terminology and Concepts](/develop/issuer/terminology-and-concepts) for how this relates to `did:empe`).
  * **Credential Issuance and Storage** Defines schemas, creates offers, signs credentials, and tracks issuance sessions.
  * **Wallet Interaction Layer** Implements QR code–based offers and the OpenID4VCI pre-authorized code flow (with optional PIN) for secure credential claiming.
  * **Security and Access Controls** All operator endpoints require a valid **OIDC JWT Bearer token** (`Authorization: Bearer <token>`), validated against a JWKS endpoint. HTTPS is required in production.

***

## Issuing a Credential

The typical issuer workflow is: create a schema, assign it to an issuer, create an offer, then let a wallet claim it. All requests below require a valid `Authorization: Bearer <token>` header.

### 1. Create a Schema

**`POST /schemas`**

**Request Body:**

```json
{
  "name": "EmployeeBadge",
  "version": "1.0",
  "description": "Employee access badge credential.",
  "formats": ["sd-jwt", "jwt-vc-json"],
  "schema": {
    "type": "object",
    "properties": {
      "employee_id": { "type": "string" },
      "full_name": { "type": "string" },
      "department": { "type": "string" }
    },
    "required": ["employee_id", "full_name", "department"]
  },
  "vct": "https://issuer.example.com/vct/employee-badge",
  "disclosureFrame": ["employee_id", "full_name", "department"]
}
```

* **name** / **version**: Identify the schema. Both are token-like (letters, digits, `.`, `_`, `-`; no spaces) and together form `EmployeeBadge@1.0`.
* **formats**: Which credential formats this schema supports — any of `sd-jwt` and `jwt-vc-json` (defaults to both).
* **schema**: A JSON-Schema describing the credential claims.
* **vct**: Optional VCT identifier used for SD-JWT credential configurations.
* **jwtVcTypes**: Optional credential `type` values used for JWT-VC-JSON configurations.
* **disclosureFrame**: Optional list of claim keys that are selectively disclosable in SD-JWT.
* **defaultValues** / **display**: Optional default claim values applied during issuance and display metadata for wallet UI.

### 2. Assign the Schema to an Issuer

**`POST /issuers/:issuerDid/schemas`**

**Request Body:**

```json
{ "schemaId": "EmployeeBadge@1.0" }
```

**Response Body:**

```json
{ "status": "assigned" }
```

### 3. Create a Credential Offer

**`POST /issuers/:issuerDid/credential-offers`**

**Request Body:**

```json
{
  "credentialConfigurationIds": ["EmployeeBadge@1.0:sd-jwt"],
  "requirePin": true,
  "issuanceMetadata": {
    "claimsByConfigurationId": {
      "EmployeeBadge@1.0:sd-jwt": {
        "employee_id": "EMP-001",
        "full_name": "Ada Lovelace",
        "department": "Engineering"
      }
    }
  }
}
```

* **credentialConfigurationIds**: One or more credential configurations to offer, each of the form `Name@Version:format`.
* **requirePin**: When `true`, a PIN (transaction code) is generated and must be entered by the recipient. Defaults to `false`.
* **issuanceMetadata**: Optional per-configuration claim values applied during issuance.

**Response Body:**

```json
{
  "credentialOfferUri": "openid-credential-offer://?credential_offer=...",
  "issuanceSessionId": "d07d0df5-0d64-4f1a-9aa7-7e7f9c2a1d70",
  "credentialConfigurationIds": ["EmployeeBadge@1.0:sd-jwt"],
  "userPin": "1234"
}
```

* **credentialOfferUri**: The `openid-credential-offer://` URI the wallet uses to claim the credential.
* **issuanceSessionId**: Identifier for tracking the issuance session.
* **credentialConfigurationIds**: The credential configurations included in the offer.
* **userPin**: Present only when `requirePin` is `true`.

To obtain a scannable QR code instead of JSON, call **`POST /issuers/:issuerDid/credential-offers/qr-code`** with the same body. It returns a **PNG image** and exposes the offer details in the `X-Credential-Offer-Uri`, `X-Issuance-Session-Id`, and (when a PIN is required) `X-Pin` response headers.

### 4. Track the Issuance Session

**`GET /issuance-sessions/:sessionId`**

Returns the session state and offer details. List all sessions for an issuer with **`GET /issuance-sessions?issuerId=<id>`**, or fetch a QR image for the session's offer URI with **`GET /issuance-sessions/:sessionId/qr-code`**.

***

## Direct Credential Signing

For server-to-server use, a credential can be signed directly without the wallet flow.

**`POST /issuers/:issuerDid/credentials/sign`**

**Request Body:**

```json
{
  "format": "sd-jwt-vc",
  "payload": {
    "vct": "https://issuer.example.com/vct/employee-badge",
    "employee_id": "EMP-001",
    "full_name": "Ada Lovelace",
    "department": "Engineering"
  },
  "subjectDid": "did:key:z6Mk...",
  "disclosureFrame": { "_sd": ["employee_id", "full_name", "department"] }
}
```

* **format**: `sd-jwt-vc` or `jwt_vc_json`. This is the *input* value; note that the response echoes the underlying claim-format identifier, not this value (see below).
* **payload**: The credential claims. For SD-JWT include a `vct`.
* **subjectDid**: Optional DID of the credential subject.
* **disclosureFrame**: SD-JWT only; lists which claims are selectively disclosable. Supplying it with `jwt_vc_json` is rejected.

**Response Body:**

```json
{
  "credential": "<compact serialized credential>",
  "format": "dc+sd-jwt"
}
```

The response `format` field reports the underlying claim-format identifier of the signed credential, which differs from the `format` value sent in the request. Signing with `sd-jwt-vc` returns `"format": "dc+sd-jwt"`, and signing with `jwt_vc_json` returns `"format": "jwt_vc"`.

***

## Authentication

All operator endpoints are protected. Requests must include a valid OIDC-issued JWT in the `Authorization` header:

```bash
Authorization: Bearer <token>
```

Tokens are validated against a JWKS endpoint (`OIDC_JWKS_URL`). During local development, authentication can be bypassed by setting `AUTH_DISABLED=true`.

<figure><img src="/files/6kaMLsgxIHlHUmsazTgv" alt="Credential-offer QR code in the operator console"><figcaption><p>The generated credential-offer QR code in the operator console</p></figcaption></figure>


# Terminology and Concepts

This glossary defines the terms used throughout the Issuer Service documentation. The Issuer Service exposes its capabilities over an HTTP API and stores its data in PostgreSQL. Operator endpoints require a valid OIDC JWT Bearer token (see **Authentication** below).

**Self-Sovereign Identity (SSI)** A user-centric approach to digital identity that lets individuals and organizations control their own identifiers and credentials without relying on a central authority.

**Decentralized Identifier (DID)** A globally unique identifier for an entity (such as an issuer, verifier, or holder). The Issuer Service supports two DID methods:

* **`did:web`** — resolved over HTTPS. The service hosts the DID document itself and serves it at `/{uuid}/did.json` (also available at `/{uuid}/.well-known/did.json`). A `did:web` looks like `did:web:issuer.example.com:<uuid>`.
* **`did:key`** — self-contained; the public key is encoded directly in the identifier, so no network lookup is needed. A `did:key` looks like `did:key:z6Mk...`.

Both methods use Ed25519 keys. `did:jwk` identifiers are also resolvable.

**DID Document** A JSON document, resolvable from a DID, that contains the public keys and verification methods needed to verify signatures produced by the DID's controller.

**EMPE DID Method and EMPE Blockchain** The `did:empe` method and the EMPE blockchain form the trust layer of the wider Empeiria ecosystem. The Issuer Service works with `did:web` and `did:key` identifiers, which it creates and serves itself over HTTPS; `did:empe` and the blockchain are separate parts of the ecosystem and are not required to run this service.

**Verifiable Credential (VC)** A cryptographically signed credential that attests to attributes of a subject. VCs follow W3C standards, ensuring integrity and authenticity. The Issuer Service issues two credential formats: SD-JWT VC and JWT-VC-JSON.

**SD-JWT VC** A Verifiable Credential encoded as a Selective Disclosure JWT (`vc+sd-jwt` / `sd-jwt-vc`). Each SD-JWT VC carries a `vct` (Verifiable Credential Type) and supports selective disclosure of individual claims.

**JWT-VC-JSON** A Verifiable Credential encoded as a standard JWT (`jwt-vc-json` / `jwt_vc_json`), typed with one or more credential types (for example `["VerifiableCredential", "EmploymentCredential"]`).

**Selective Disclosure** A property of SD-JWT VCs that lets a holder reveal only a chosen subset of claims to a verifier while keeping the rest hidden, without breaking the issuer's signature. The claims that may be disclosed are defined by the schema's disclosure frame (for example `["employee_id", "full_name", "department"]`).

**Credential Schema** A JSON-based template that defines the structure, properties, and required fields of a credential type. A schema is identified by a `name` and `version` (token-like values matching `^[A-Za-z0-9][A-Za-z0-9._-]*$`, no spaces) and contains a JSON Schema describing the claims. Schemas are versioned so credential definitions can evolve. For example, a schema named `EmployeeBadge` at version `1.0` requires `employee_id`, `full_name`, and `department`.

**Credential Configuration** The combination of a schema version and a credential format, identified as `Name@Version:format` — for example `EmployeeBadge@1.0:sd-jwt`. A schema that supports both formats yields two credential configurations (`...:sd-jwt` and `...:jwt-vc-json`). Credential offers reference credentials by their credential configuration id.

**OpenID4VCI (OpenID for Verifiable Credential Issuance)** The standards-based protocol the Issuer Service uses to deliver credentials to wallets. The service implements the pre-authorized code flow with an optional PIN.

**Credential Offer** An invitation, created by the issuer, for a wallet to claim one or more credentials. An offer references one or more credential configuration ids and is delivered as an `openid-credential-offer://` URI (returned as `credentialOfferUri`) or as a QR-code PNG.

**Pre-Authorized Code Flow** The OpenID4VCI flow used for issuance. The issuer mints a credential offer that already contains a pre-authorized code, so the wallet can claim the credential without a separate interactive login. Set `requirePin: true` on the offer to additionally require a PIN.

**PIN (Transaction Code)** An out-of-band code that the holder must enter to claim a credential when `requirePin` is set. When a PIN is required, it is returned as `userPin` in the offer response and as the `X-Pin` header on the QR-code response.

**Issuance Metadata** Optional data attached to a credential offer that supplies the claim values for each credential configuration. It is passed as `issuanceMetadata` on the offer, typically as `{ "claimsByConfigurationId": { "EmployeeBadge@1.0:sd-jwt": { "employee_id": "EMP-001", "full_name": "Ada Lovelace", "department": "Engineering" } } }`.

**Issuance Session** A server-side record that tracks the state of a single credential offer from creation through claiming. Each offer returns an `issuanceSessionId`; sessions can be listed per issuer and inspected individually, including a regenerable QR code for the offer URI.

**Issuer** A registered entity, bound to an existing DID, that signs and issues credentials. An issuer is created from a DID, can have one or more schemas assigned to it, and serves its own OpenID4VCI issuer metadata document (the issuer's `.well-known/did-configuration`).

**Direct Credential Signing** A flow that signs a credential directly, without the wallet/offer cycle. The caller supplies a `format` (`sd-jwt-vc` or `jwt_vc_json`), a claims `payload` (including `vct` for SD-JWT), an optional `subjectDid`, and an optional `disclosureFrame` for SD-JWT. The service returns the signed credential and its format.

**Holder (Cloud Wallet)** A wallet hosted and managed by the service on behalf of a single user. A holder can claim offered credentials and present them to verifiers. Ownership is enforced, so each user can access only their own holders.

**One-Click Deployment Portal** A provisioning tool that deploys an Issuer Service instance and its operator admin panel, then helps with initial configuration. It provisions the running service; DID and issuer creation are explicit admin API calls made after deployment.

**Authentication** Operator endpoints are protected by OIDC JWT Bearer tokens validated against a JWKS endpoint. Each request must include an `Authorization: Bearer <token>` header. The agent/DID and holder (wallet) routes additionally require the `admin` role in the token's `realm_access.roles` or in a `resource_access` client-role entry; the schema, credential-offer, issuance-session, and signing endpoints require a valid token but no specific role (see [Authentication](/develop/issuer#authentication)).


# Issuers and Credential Configurations

An **issuer** is a registered entity, bound to an existing DID, that signs and issues credentials. A **credential configuration** describes exactly what an issuer can issue — the combination of a schema version and a credential format. This page explains how to register an issuer, how schemas and formats produce credential configurations, and how those configurations are referenced when you create credential offers. It also covers listing and inspecting issuers and reading an issuer's OpenID4VCI metadata document.

All `/agent` endpoints require an admin Bearer token (`Authorization: Bearer <token>`), and the token must carry the `admin` role in its `realm_access.roles` or in a `resource_access` client-role entry. The issuer endpoints documented here that are not under `/agent` — `POST /issuers/:issuerDid/schemas` and `POST /issuers/:issuerDid/credential-offers` — require a valid Bearer token but no specific role: they validate the token but do not enforce a specific realm role (see [Authentication](/develop/issuer#authentication)).

## Registering an issuer

An issuer is created from a DID that already exists. Create the DID first with `POST /agent/did/web` or `POST /agent/did/key` (see **DID Document Management**), then bind an issuer to it. The supplied DID becomes both the issuer identifier and its signing identity, so the service can sign credentials and issuer-metadata tokens on its behalf. Register issuers against the `did:web` or `did:key` identifiers the service creates itself (see [Terminology and Concepts](/develop/issuer/terminology-and-concepts) for the ecosystem's `did:empe` method).

**`POST /agent/issuer`**

**Request Body**:

```json
{
  "did": "did:web:issuer.example.com:5f1c2e88-9a3d-4f2b-8c7a-2d4e6f8a1b3c"
}
```

* **did**: An existing DID that will act as the issuer identifier and signing key. The DID must already exist before this call.

**Response Body**:

```json
{
  "issuerId": "did:web:issuer.example.com:5f1c2e88-9a3d-4f2b-8c7a-2d4e6f8a1b3c",
  "credentialConfigurationsSupported": {}
}
```

* **issuerId**: The issuer identifier, which is the DID you registered.
* **credentialConfigurationsSupported**: A map of the credential configurations this issuer can issue, keyed by credential configuration id. It reflects the credential configurations currently available from active schemas; on a clean deployment with no schemas yet it is empty (`{}`).
* The optional fields **authorizationServerConfigs**, **display**, **dpopSigningAlgValuesSupported**, and **batchCredentialIssuance** are absent from the JSON until configured — they are never returned as empty arrays or `null`.

If active schemas already exist when the issuer is registered, the new issuer is created with those configurations. The next step is to make sure the schemas you want to issue are available, which is what produces credential configurations.

## From schema and format to a credential configuration

A **credential schema** defines the claims of a credential type; it is identified by `Name@Version` (for example `EmployeeBadge@1.0`) and declares one or more formats in its `formats` field (`sd-jwt`, `jwt-vc-json`, or both — see **Schema Management**). Each schema-and-format pair produces a **credential configuration id** of the form `Name@Version:format`:

* `EmployeeBadge@1.0:sd-jwt` — the `EmployeeBadge` version `1.0` schema issued as an SD-JWT VC.
* `EmployeeBadge@1.0:jwt-vc-json` — the same schema issued as a JWT-VC-JSON credential.

A schema that declares both formats therefore yields two credential configurations. Assigning a schema to an issuer refreshes that issuer's metadata so its `credentialConfigurationsSupported` map reflects the configurations of all active schemas — including the one you just assigned — rather than an isolated per-issuer subset.

**`POST /issuers/:issuerDid/schemas`**

* **:issuerDid**: The issuer DID (URL-encode the value if it contains characters such as `:`).

**Request Body**:

```json
{
  "schemaId": "EmployeeBadge@1.0"
}
```

* **schemaId**: The schema identifier in `Name@Version` format. The schema must already exist and be active.

**Response Body**:

```json
{
  "status": "assigned"
}
```

After assignment, the issuer's metadata is refreshed to include a credential configuration entry per supported format of every active schema. For an SD-JWT VC, the entry carries the format, the credential type (`vct`), supported cryptographic binding methods, the signing algorithm, and a `claims` map derived from the schema's properties:

```json
{
  "EmployeeBadge@1.0:sd-jwt": {
    "format": "vc+sd-jwt",
    "vct": "https://issuer.example.com/vct/employee-badge",
    "scope": "openid4vc:credential:EmployeeBadge@1.0",
    "cryptographic_binding_methods_supported": ["jwk", "did:key", "did:jwk", "did:web"],
    "credential_signing_alg_values_supported": ["EdDSA"],
    "proof_types_supported": {
      "jwt": { "proof_signing_alg_values_supported": ["EdDSA"] }
    },
    "claims": {
      "employee_id": { "display": [{ "name": "employee_id" }] },
      "full_name": { "display": [{ "name": "full_name" }] },
      "department": { "display": [{ "name": "department" }] }
    }
  }
}
```

For a JWT-VC-JSON credential, the entry instead carries a `credential_definition` with the credential `type` array:

```json
{
  "EmployeeBadge@1.0:jwt-vc-json": {
    "format": "jwt_vc_json",
    "scope": "openid4vc:credential:EmployeeBadge@1.0",
    "cryptographic_binding_methods_supported": ["did:key", "did:jwk", "did:web"],
    "credential_signing_alg_values_supported": ["EdDSA"],
    "credential_definition": {
      "type": ["VerifiableCredential", "EmploymentCredential"]
    },
    "proof_types_supported": {
      "jwt": { "proof_signing_alg_values_supported": ["EdDSA"] }
    }
  }
}
```

If the SD-JWT schema does not define a `vct`, the service derives one from the schema id under the issuer base URL (`<ISSUER_BASE_URL>/schemas/<encoded-schemaId>`). The credential type array for JWT-VC-JSON comes from the schema's `jwtVcTypes`, falling back to `["VerifiableCredential", "<name>"]`. The `scope` value is always `openid4vc:credential:<Name@Version>`.

## Referencing configurations from credential offers

Credential offers reference credentials by their credential configuration id, never by schema name alone. When you create an offer, list the configuration ids in `credentialConfigurationIds` (see **Issuing Credentials & Interacting with Wallets**):

**`POST /issuers/:issuerDid/credential-offers`**

**Request Body**:

```json
{
  "credentialConfigurationIds": ["EmployeeBadge@1.0:sd-jwt"],
  "requirePin": false,
  "issuanceMetadata": {
    "claimsByConfigurationId": {
      "EmployeeBadge@1.0:sd-jwt": {
        "employee_id": "EMP-001",
        "full_name": "Ada Lovelace",
        "department": "Engineering"
      }
    }
  }
}
```

* **credentialConfigurationIds**: One or more credential configuration ids the wallet may claim, each in `Name@Version:format` form. Every id must be assigned to this issuer.
* **issuanceMetadata.claimsByConfigurationId**: The claim values for each configuration id, supplied per configuration so a single offer can include multiple credentials.

Only configuration ids that appear in the issuer's `credentialConfigurationsSupported` map can be offered. Creating a new version of the same schema name (for example `EmployeeBadge@1.1`) supersedes the old version: after the next metadata refresh the old version's configuration ids are removed from `credentialConfigurationsSupported`, and unclaimed offers referencing them fail at claim time with `Schema <id> is not active.`. Credentials that were already issued remain valid.

## Listing and inspecting issuers

**`GET /agent/issuers`**

Returns all registered issuers.

**Response Body**:

```json
[
  {
    "issuerId": "did:web:issuer.example.com:5f1c2e88-9a3d-4f2b-8c7a-2d4e6f8a1b3c",
    "credentialConfigurationsSupported": {
      "EmployeeBadge@1.0:sd-jwt": { "format": "vc+sd-jwt", "...": "..." }
    }
  }
]
```

**`GET /agent/issuer/:did`**

Returns a single issuer record by issuer DID.

* **:did**: The issuer DID (URL-encode the value if it contains characters such as `:`).

**Response Body**:

```json
{
  "issuerId": "did:web:issuer.example.com:5f1c2e88-9a3d-4f2b-8c7a-2d4e6f8a1b3c",
  "credentialConfigurationsSupported": {
    "EmployeeBadge@1.0:sd-jwt": { "format": "vc+sd-jwt", "...": "..." },
    "EmployeeBadge@1.0:jwt-vc-json": { "format": "jwt_vc_json", "...": "..." }
  }
}
```

* **issuerId**: The issuer DID.
* **credentialConfigurationsSupported**: The credential configurations this issuer can issue, keyed by credential configuration id — these are exactly the ids you reference from credential offers.

## Issuer metadata document

Each registered issuer also exposes an OpenID4VCI issuer metadata document. Wallets read this document during issuance to learn the credential issuer identifier, the credential endpoint, the supported credential configurations, and the authorization server they should obtain an access token from. This is issuer metadata used by wallets — it is not a domain-linkage or trust mechanism.

**`GET /agent/issuer/:did/.well-known/did-configuration`**

* **:did**: The issuer DID (URL-encode the value if it contains characters such as `:`).

The response has a `credentialIssuer` object (the OpenID4VCI Credential Issuer Metadata) and an `authorizationServers` array (the authorization-server metadata wallets use to obtain an access token), alongside `originalDraftVersion`, which reports the OpenID4VCI draft the metadata is shaped for. The endpoint URLs are formed by joining `ISSUER_BASE_URL` with the issuer DID, so each issuer has its own metadata namespace.

**Response Body**:

```json
{
  "originalDraftVersion": "V1",
  "credentialIssuer": {
    "credential_issuer": "https://issuer.example.com/did:web:issuer.example.com:5f1c2e88-9a3d-4f2b-8c7a-2d4e6f8a1b3c",
    "credential_endpoint": "https://issuer.example.com/did:web:issuer.example.com:5f1c2e88-9a3d-4f2b-8c7a-2d4e6f8a1b3c/credential",
    "deferred_credential_endpoint": "https://issuer.example.com/did:web:issuer.example.com:5f1c2e88-9a3d-4f2b-8c7a-2d4e6f8a1b3c/deferred-credential",
    "nonce_endpoint": "https://issuer.example.com/did:web:issuer.example.com:5f1c2e88-9a3d-4f2b-8c7a-2d4e6f8a1b3c/nonce",
    "credential_configurations_supported": {
      "EmployeeBadge@1.0:sd-jwt": {
        "format": "vc+sd-jwt",
        "vct": "https://issuer.example.com/vct/employee-badge",
        "scope": "openid4vc:credential:EmployeeBadge@1.0",
        "cryptographic_binding_methods_supported": ["jwk", "did:key", "did:jwk", "did:web"],
        "credential_signing_alg_values_supported": ["EdDSA"],
        "proof_types_supported": {
          "jwt": { "proof_signing_alg_values_supported": ["EdDSA"] }
        },
        "claims": {
          "employee_id": { "display": [{ "name": "employee_id" }] },
          "full_name": { "display": [{ "name": "full_name" }] },
          "department": { "display": [{ "name": "department" }] }
        }
      }
    }
  },
  "authorizationServers": [
    {
      "issuer": "https://issuer.example.com/did:web:issuer.example.com:5f1c2e88-9a3d-4f2b-8c7a-2d4e6f8a1b3c",
      "token_endpoint": "https://issuer.example.com/did:web:issuer.example.com:5f1c2e88-9a3d-4f2b-8c7a-2d4e6f8a1b3c/token",
      "jwks_uri": "https://issuer.example.com/did:web:issuer.example.com:5f1c2e88-9a3d-4f2b-8c7a-2d4e6f8a1b3c/jwks",
      "pre-authorized_grant_anonymous_access_supported": true,
      "grant_types_supported": [
        "authorization_code",
        "urn:ietf:params:oauth:grant-type:pre-authorized_code"
      ]
    }
  ]
}
```

* **originalDraftVersion**: The OpenID4VCI draft version the metadata document is shaped for.
* **credentialIssuer.credential\_issuer**: The credential issuer identifier — `ISSUER_BASE_URL` joined with the issuer DID.
* **credentialIssuer.credential\_endpoint**: The endpoint a wallet calls to request the credential after exchanging the pre-authorized code.
* **credentialIssuer.credential\_configurations\_supported**: The credential configurations on offer, keyed by credential configuration id. This map mirrors the configurations returned by `GET /agent/issuer/:did`, so wallets and offers reference the same `Name@Version:format` keys.
* **authorizationServers**: The authorization-server metadata, including the `token_endpoint`, `jwks_uri`, and the supported grant types. The OpenID4VCI pre-authorized code grant (`urn:ietf:params:oauth:grant-type:pre-authorized_code`) that backs the offer flow is advertised here.

The response can carry a few additional fields beyond those shown above. Inside `credentialIssuer` it may also include `authorization_servers` (a string array of authorization-server identifiers), `batch_credential_issuance` (batch-issuance parameters), and `display` (issuer-level display metadata). At the top level it can carry `knownCredentialConfigurations` (the same configuration map) and `signedMetadataJwt` (a signed copy of the metadata). The contents of `display` and the value of `signedMetadataJwt` depend on how a given issuer is configured.


# DID Document Management

The Issuer Service creates and manages the DIDs that identify an issuer and sign its credentials. DIDs are created explicitly through the API by an administrator — they are not generated automatically on deployment. The service supports two DID methods: **`did:web`** (whose DID Documents are stored by the service and served over HTTPS) and **`did:key`** (a self-describing key-based identifier). DID Documents use Ed25519 verification keys, exposed for both `authentication` and `assertionMethod`.

All `/agent` endpoints require an admin Bearer token (`Authorization: Bearer <token>`; see [Authentication](/develop/issuer#authentication)).

## Creating a did:web

A `did:web` is hosted by the service. A random UUID is appended to the supplied domain to form the identifier, a fresh Ed25519 key is generated, and the resulting DID Document is stored and served over HTTPS.

**`POST /agent/did/web`**

**Request Body**:

```json
{
  "domain": "issuer.example.com"
}
```

* **domain**: The hostname for the `did:web` identifier (e.g. `issuer.example.com`, optionally with a port such as `issuer.example.com:8080`). It must be a valid hostname.

**Response Body**:

```json
{
  "did": "did:web:issuer.example.com:5f1c2e88-9a3d-4f2b-8c7a-2d4e6f8a1b3c",
  "didDocument": {
    "@context": [
      "https://www.w3.org/ns/did/v1",
      "https://w3id.org/security/suites/ed25519-2018/v1"
    ],
    "id": "did:web:issuer.example.com:5f1c2e88-9a3d-4f2b-8c7a-2d4e6f8a1b3c",
    "verificationMethod": [
      {
        "id": "did:web:issuer.example.com:5f1c2e88-9a3d-4f2b-8c7a-2d4e6f8a1b3c#key-1",
        "type": "Ed25519VerificationKey2018",
        "controller": "did:web:issuer.example.com:5f1c2e88-9a3d-4f2b-8c7a-2d4e6f8a1b3c",
        "publicKeyBase58": "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV"
      }
    ],
    "authentication": [
      "did:web:issuer.example.com:5f1c2e88-9a3d-4f2b-8c7a-2d4e6f8a1b3c#key-1"
    ],
    "assertionMethod": [
      "did:web:issuer.example.com:5f1c2e88-9a3d-4f2b-8c7a-2d4e6f8a1b3c#key-1"
    ]
  }
}
```

* **did**: The created identifier, of the form `did:web:<domain>:<uuid>`.
* **didDocument**: The full DID Document, including the Ed25519 verification method (`key-1`) referenced by both `authentication` and `assertionMethod`.

The DID Document is persisted by the service and published at a public well-known location (see [Hosted did:web resolution](#hosted-didweb-resolution)).

## Creating a did:key

A `did:key` encodes an Ed25519 public key directly in the identifier, so it is resolvable without hosting.

**`POST /agent/did/key`**

This endpoint takes no request body.

**Response Body**:

```json
{
  "did": "did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH",
  "verificationMethodIds": [
    "did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH#z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH"
  ]
}
```

* **did**: The created `did:key` identifier.
* **verificationMethodIds**: Verification method identifiers that can be used for signing and assertions.

Once a DID exists, you register it as an issuer with `POST /agent/issuer` (body `{ "did": "..." }`) before assigning schemas or creating credential offers. A schema and format then yield a credential configuration id such as `EmployeeBadge@1.0:sd-jwt`, which credential offers reference.

## Resolving and listing DIDs

**`GET /agent/did/:did`**

Resolves a DID created by the service and returns its current DID Document. URL-encode the `:did` value if it contains characters such as `:`.

**Response Body**:

```json
{
  "did": "did:web:issuer.example.com:5f1c2e88-9a3d-4f2b-8c7a-2d4e6f8a1b3c",
  "didDocument": { "...": "..." }
}
```

For a DID the service does not know, the endpoint returns `200 OK` with a `null` body rather than a 404.

**`GET /agent/dids`**

Returns the list of DIDs created by the service.

**Response Body**:

```json
[
  "did:web:issuer.example.com:5f1c2e88-9a3d-4f2b-8c7a-2d4e6f8a1b3c",
  "did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH"
]
```

## Hosted did:web resolution

`did:web` DID Documents are stored by the service and are publicly resolvable over HTTPS — no authentication is required, and these routes are exempt from rate limiting. A `did:web:<domain>:<uuid>` resolves to:

**`GET /:uuid/did.json`**

This is the URL a `did:web` resolver derives from a path-based DID such as `did:web:<domain>:<uuid>`. The alias **`GET /:uuid/.well-known/did.json`** serves the same document. The response is the stored DID Document JSON. Resolving a UUID that does not exist returns `404 Not Found`.

## Key material

Verification keys are Ed25519 (`Ed25519VerificationKey2018`) and are generated and held by the service's internal key-management system; private key material is never exposed through the API. The DID Documents are stored by the service; `did:web` documents are persisted and served over HTTPS. Each issuer DID signs credentials with the key referenced by its `assertionMethod`. Key rotation and service-endpoint updates on an existing DID Document are planned future capabilities.

## Issuer metadata document

Each registered issuer exposes an OpenID4VCI issuer metadata document. This is issuer metadata used by wallets during issuance — it is not a domain-linkage or trust mechanism.

**`GET /agent/issuer/:did/.well-known/did-configuration`**

* **:did**: The issuer DID (URL-encode the value if it contains characters such as `:`).

The response is the issuer's OpenID4VCI metadata document, including the credential issuer identifier, the credential endpoint, and the supported credential configurations (e.g. `EmployeeBadge@1.0:sd-jwt`). For a full worked example of this response body, see [Issuers and Credential Configurations](/develop/issuer/issuer-and-credential-configurations#issuer-metadata-document).


# Schema Management

Credential schemas define the structure and required claims of the credentials an issuer can produce, ensuring consistency and interoperability across wallets and verifiers. Each schema is stored by the service under a stable identifier and, once assigned to an issuer, becomes one or more **credential configurations** that credential offers reference.

All schema endpoints require authentication: send an OIDC JWT in the `Authorization: Bearer <token>` header. These endpoints validate the token but do not enforce a specific realm role, so any authenticated caller may use them (see [Authentication](/develop/issuer#authentication)).

***

## 1. Schema Identifiers and Credential Configurations

A schema is identified by its name and version joined with `@`:

Both `name` and `version` are caller-supplied tokens matching `^[A-Za-z0-9][A-Za-z0-9._-]*$` (no spaces).

Each active schema produces a **credential configuration id** for every format it declares, in the form `<name>@<version>:<format>`:

* `EmployeeBadge@1.0:sd-jwt`
* `EmployeeBadge@1.0:jwt-vc-json`

These configuration ids are what you pass to the credential-offer endpoint. The direct-signing endpoint does not take a configuration id; it instead takes a `format` value (`sd-jwt-vc` or `jwt_vc_json`) plus a `payload`. The available schema formats are `sd-jwt` (SD-JWT VC) and `jwt-vc-json` (JWT-VC-JSON); if `formats` is omitted, both are generated.

***

## 2. Create Schema

```
POST /schemas
Authorization: Bearer <token>
Content-Type: application/json
```

### Request Body

* **`name`** (string, required): Schema name. Token-like (`^[A-Za-z0-9][A-Za-z0-9._-]*$`), max 120 characters. *Example:* `"EmployeeBadge"`
* **`version`** (string, required): Schema version. Token-like, max 50 characters. Supplied explicitly by the caller (it is not auto-incremented). *Example:* `"1.0"`
* **`description`** (string, optional): Human-readable description, max 500 characters. *Example:* `"Employee access badge credential."`
* **`formats`** (array of strings, optional): Credential formats this schema supports. Each value must be `"sd-jwt"` or `"jwt-vc-json"`, and at least one value is required when the field is present. Defaults to `["sd-jwt", "jwt-vc-json"]`.
* **`schema`** (object, required): A JSON Schema describing the credential claims. If a top-level `type` is present it must be (or include) `"object"`; `properties` (if present) must be an object; `required` (if present) must be an array, and every key it lists must exist in `properties`.
* **`vct`** (string, optional): VCT (Verifiable Credential Type) identifier used for SD-JWT credential configurations, max 500 characters. *Example:* `"https://issuer.example.com/vct/employee-badge"`
* **`jwtVcTypes`** (array of strings, optional): Credential `type` values for JWT-VC-JSON configurations. If omitted while a JWT-VC-JSON format is enabled, it defaults to `["VerifiableCredential", "<name>"]`.
* **`defaultValues`** (object, optional): Default claim values applied during issuance when not supplied per offer. *Example:* `{ "company": "EMPE" }`
* **`disclosureFrame`** (array of strings, optional): Claim keys that are selectively disclosable for SD-JWT issuance.
* **`display`** (array of objects, optional): Display metadata used by wallets to render the credential. Each entry requires `name` and may also include `locale`, `description`, `backgroundColor`, and `textColor`.

### Example Request

```json
{
  "name": "EmployeeBadge",
  "version": "1.0",
  "description": "Employee access badge credential.",
  "formats": ["sd-jwt"],
  "vct": "https://issuer.example.com/vct/employee-badge",
  "schema": {
    "type": "object",
    "properties": {
      "employee_id": { "type": "string" },
      "full_name":   { "type": "string" },
      "department":  { "type": "string" },
      "access_level": { "type": "string" }
    },
    "required": ["employee_id", "full_name", "department"]
  },
  "defaultValues": { "company": "EMPE" },
  "disclosureFrame": ["employee_id", "full_name", "department", "access_level"],
  "display": [
    {
      "name": "Employee Badge",
      "description": "Access badge for internal systems.",
      "backgroundColor": "#0f172a",
      "textColor": "#f8fafc"
    }
  ]
}
```

### Response Body

On success the API responds with the stored schema record:

```json
{
  "schemaId": "EmployeeBadge@1.0",
  "name": "EmployeeBadge",
  "version": "1.0",
  "status": "active",
  "schema": {
    "name": "EmployeeBadge",
    "version": "1.0",
    "description": "Employee access badge credential.",
    "formats": ["sd-jwt"],
    "vct": "https://issuer.example.com/vct/employee-badge",
    "schema": {
      "type": "object",
      "properties": {
        "employee_id": { "type": "string" },
        "full_name":   { "type": "string" },
        "department":  { "type": "string" },
        "access_level": { "type": "string" }
      },
      "required": ["employee_id", "full_name", "department"]
    },
    "defaultValues": { "company": "EMPE" },
    "disclosureFrame": ["employee_id", "full_name", "department", "access_level"],
    "display": [
      {
        "name": "Employee Badge",
        "description": "Access badge for internal systems.",
        "backgroundColor": "#0f172a",
        "textColor": "#f8fafc"
      }
    ]
  },
  "createdAt": "2024-01-01T12:00:00.000Z",
  "updatedAt": "2024-01-01T12:00:00.000Z"
}
```

* **`schemaId`** (string): The `name@version` identifier.
* **`name`** (string): The schema name.
* **`version`** (string): The caller-supplied version.
* **`status`** (string): Either `"active"` or `"superseded"` (see Versioning).
* **`schema`** (object): The normalized schema definition, including any defaults the service applied (such as `formats` and `jwtVcTypes`).
* **`createdAt`** / **`updatedAt`** (string): ISO 8601 timestamps.

Creating a schema whose `name@version` already exists is rejected with HTTP 409 Conflict. A malformed `schema` definition is rejected with HTTP 400 Bad Request.

***

## 3. Assign a Schema to an Issuer

Creating a schema stores its definition but does not yet expose it for issuance. To make a schema's credential configurations available on an issuer, assign it. This refreshes the issuer's metadata so it advertises the configurations derived from active schemas.

```
POST /issuers/{issuerDid}/schemas
Authorization: Bearer <token>
Content-Type: application/json
```

### Request Body

* **`schemaId`** (string, required): The schema identifier in `name@version` format. *Example:* `"EmployeeBadge@1.0"`

### Example Request

```json
{
  "schemaId": "EmployeeBadge@1.0"
}
```

### Response Body

```json
{
  "status": "assigned"
}
```

After assignment, the issuer exposes `EmployeeBadge@1.0:sd-jwt` (and `EmployeeBadge@1.0:jwt-vc-json` if that format is enabled), which can then be referenced when creating credential offers. Assignment fails with HTTP 404 Not Found if the schema does not exist, and HTTP 410 Gone if the referenced version has been superseded.

***

## 4. List Schemas

```
GET /schemas?name={name}&includeInactive={true|false}
Authorization: Bearer <token>
```

* **`name`** (query, optional): Filter to schemas with this exact name.
* **`includeInactive`** (query, optional): When `true`, superseded schemas are also returned. Defaults to `false` (active only).

Results are returned newest-first by creation time.

### Example Response

```json
[
  {
    "schemaId": "EmployeeBadge@1.0",
    "name": "EmployeeBadge",
    "version": "1.0",
    "status": "active",
    "schema": { "...": "schema definition" },
    "createdAt": "2024-01-01T12:00:00.000Z",
    "updatedAt": "2024-01-01T12:00:00.000Z"
  }
]
```

***

## 5. List Schema Versions

Returns every stored version for a schema name, including superseded ones — useful for auditing or migrating schema changes.

```
GET /schemas/{schemaName}/versions
Authorization: Bearer <token>
```

### Example Response

```json
[
  {
    "schemaId": "EmployeeBadge@1.1",
    "name": "EmployeeBadge",
    "version": "1.1",
    "status": "active",
    "schema": { "...": "schema definition" },
    "createdAt": "2024-02-01T09:00:00.000Z",
    "updatedAt": "2024-02-01T09:00:00.000Z"
  },
  {
    "schemaId": "EmployeeBadge@1.0",
    "name": "EmployeeBadge",
    "version": "1.0",
    "status": "superseded",
    "schema": { "...": "schema definition" },
    "createdAt": "2024-01-01T12:00:00.000Z",
    "updatedAt": "2024-02-01T09:00:00.000Z"
  }
]
```

***

## 6. Get a Schema by ID

```
GET /schemas/{schemaId}?includeInactive={true|false}
Authorization: Bearer <token>
```

* **`schemaId`** (path): The `name@version` identifier, e.g. `EmployeeBadge@1.0`.
* **`includeInactive`** (query, optional): When `true`, a superseded version is still returned. Defaults to `false`.

A missing schema returns HTTP 404 Not Found. A superseded version returns HTTP 410 Gone unless `includeInactive=true`.

***

## 7. Versioning

Versions are **explicit string versions supplied by the caller**, not auto-incrementing integers. To evolve a schema, create a new version under the same name:

* Creating the same `name@version` twice is rejected with HTTP 409 Conflict.
* Creating a **new** version of an existing name automatically marks the previously active version as `superseded`; the new version becomes `active`.
* A superseded version returns HTTP 410 Gone from `GET /schemas/:schemaId` unless `includeInactive=true` is supplied. Assignment (`POST /issuers/:issuerDid/schemas`) always rejects a superseded version with 410 — it accepts no `includeInactive` option. (The versions list always includes superseded entries.)
* Credentials already issued against an older version remain valid — superseding only affects which configurations an issuer advertises going forward. Re-assign the new version to an issuer to advertise its configurations.

***

## 8. Note on Deletion

There is no delete endpoint for schemas. Schemas are retired by creating a newer version that supersedes them, which preserves an auditable history of every definition.

***

## Security

Schema creation, assignment, listing, and retrieval all require a valid OIDC JWT Bearer token; these endpoints require authentication but do not enforce a specific realm role. This ensures only authenticated operators can define and assign credential definitions, preserving the integrity and trustworthiness of what an issuer can issue.


# Issuing Credentials and Interacting With Wallets

Issuing a credential means creating a **credential offer** for one or more credential configurations and letting a recipient claim it into their wallet. The platform implements the **OpenID4VCI pre-authorized code flow**: the wallet scans a QR code (or follows an `openid-credential-offer://` deep link), optionally enters a PIN, and the issuer signs and returns the credential in the configured format (SD-JWT VC or JWT-VC-JSON). This page walks through the full flow against the live API.

All endpoints below require a valid Bearer token, sent as `Authorization: Bearer <token>` (see [Authentication](/develop/issuer#authentication)).

***

## 1. Prerequisites

Before you can create a credential offer you need:

* **An issuer DID** — a `did:web` or `did:key` created through the agent API and hosted/served by the service.
* **A registered issuer** — bound to that DID (`POST /agent/issuer`).
* **At least one schema assigned to the issuer** — assigning a schema produces one or more **credential configurations**.

A schema combined with a format yields a **credential configuration id** of the form `Name@Version:format`, for example `EmployeeBadge@1.0:sd-jwt`. Credential offers reference these configuration ids. See the schema documentation for how to create and assign schemas.

In the examples below, replace `:issuerDid` with your issuer's DID (e.g. `did:web:issuer.example.com:1f2e...` or `did:key:z6Mk...`).

***

## 2. Create a Credential Offer

```
POST /issuers/:issuerDid/credential-offers
Authorization: Bearer <token>
Content-Type: application/json
```

### Request Body

* **`credentialConfigurationIds`** (array of strings, required): One or more credential configuration ids to include in the offer. At least one is required. *Example:* `["EmployeeBadge@1.0:sd-jwt"]`
* **`requirePin`** (boolean, optional): When `true`, the pre-authorized code is protected by a PIN (transaction code) that the holder must enter in their wallet. Defaults to `false`.
* **`issuanceMetadata`** (object, optional): Metadata passed to the credential mapper. Use `claimsByConfigurationId` to supply the claim values for each configuration in the offer.

**Example Request**

```json
{
  "credentialConfigurationIds": ["EmployeeBadge@1.0:sd-jwt"],
  "requirePin": true,
  "issuanceMetadata": {
    "claimsByConfigurationId": {
      "EmployeeBadge@1.0:sd-jwt": {
        "employee_id": "EMP-001",
        "full_name": "Ada Lovelace",
        "department": "Engineering"
      }
    }
  }
}
```

### Response Body

```json
{
  "credentialOfferUri": "openid-credential-offer://?credential_offer=...",
  "issuanceSessionId": "d07d0df5-0d64-4f1a-9aa7-7e7f9c2a1d70",
  "credentialConfigurationIds": ["EmployeeBadge@1.0:sd-jwt"],
  "userPin": "1234"
}
```

* **`credentialOfferUri`** (string): An `openid-credential-offer://` URI. Render it as a QR code or present it as a deep link so a wallet can start the issuance flow.
* **`issuanceSessionId`** (string): The identifier of the issuance session created for this offer. Use it to track issuance progress (see [Track Issuance Sessions](#5-track-issuance-sessions)).
* **`credentialConfigurationIds`** (array of strings): The credential configuration ids included in the offer.
* **`userPin`** (string, optional): Returned only when `requirePin` is `true`. Deliver this PIN to the holder out-of-band; they must enter it in their wallet to claim the credential.

***

## 3. Generate a QR Code

To render the offer as a scannable image without building your own QR encoder, call the QR-code variant. It accepts the **same request body** as the offer endpoint and returns the QR directly as a PNG.

```
POST /issuers/:issuerDid/credential-offers/qr-code
Authorization: Bearer <token>
Content-Type: application/json
```

The response is an `image/png` body (a 400×400 QR encoding the `credentialOfferUri`). The offer details are returned in response headers:

* **`X-Credential-Offer-Uri`** — the `openid-credential-offer://` URI encoded in the QR.
* **`X-Issuance-Session-Id`** — the issuance session id for this offer.
* **`X-Pin`** — the PIN, present only when `requirePin` was `true`.

You can also regenerate a QR for an existing session at any time:

```
GET /issuance-sessions/:sessionId/qr-code
Authorization: Bearer <token>
```

This returns an `image/png` for the session's offer URI, with the `X-Credential-Offer-Uri` header set.

<figure><img src="/files/6kaMLsgxIHlHUmsazTgv" alt="Rendered credential-offer QR code"><figcaption><p>A rendered credential-offer QR code in the operator console</p></figcaption></figure>

***

## 4. Wallet Claims the Credential

Once the offer is presented, the recipient claims it with an SSI-compatible wallet:

1. The wallet **scans the QR code** or **opens the `openid-credential-offer://` link**.
2. It reads the credential offer and the issuer metadata, then runs the **OpenID4VCI pre-authorized code flow** against the issuer's authorization server.
3. If the offer required a PIN, the wallet **prompts the holder for the PIN** (the value returned as `userPin` / the `X-Pin` header).
4. The issuer **signs the credential** for the requested configuration and returns it to the wallet in the configured format — **SD-JWT VC** or **JWT-VC-JSON**.
5. The wallet **stores the credential** for later presentation to verifiers.

The wallet handles token exchange and credential retrieval internally as part of the OpenID4VCI flow; there is no separate client-side token endpoint to call. From the issuer's side, you only create the offer and (optionally) track the session.

Two things happen automatically at claim time:

* The service injects an **`authorized_user`** claim (`authorizedUser` in the JWT-VC-JSON credential subject) into every issued credential payload, set from the `sub` of the wallet's access token, so each credential records which authenticated user claimed it.
* For custom schemas, the claim values are **validated against the schema**: required claims must be present and each claim must match its declared type. A validation failure aborts issuance and moves the session to the `Error` state.

> The platform also provides a cloud (server-side) wallet. A holder tenant can claim an offer programmatically by posting the offer URI to `POST /holders/:holderId/process` with body `{ "uri": "openid-credential-offer://..." }`. See the holder/wallet documentation for details.

***

## 5. Track Issuance Sessions

Each offer creates an issuance session whose state advances as the wallet completes the flow.

**List sessions for an issuer**

```
GET /issuance-sessions?issuerId=<issuerId>
Authorization: Bearer <token>
```

**Get a single session**

```
GET /issuance-sessions/:sessionId
Authorization: Bearer <token>
```

### Response Body

```json
{
  "id": "d07d0df5-0d64-4f1a-9aa7-7e7f9c2a1d70",
  "issuerId": "did:web:issuer.example.com:1f2e...",
  "state": "OfferCreated",
  "credentialOfferUri": "openid-credential-offer://?credential_offer=...",
  "createdAt": "2024-08-09T12:34:56.789Z",
  "expiresAt": "2024-08-09T13:34:56.789Z",
  "userPin": "1234",
  "issuedCredentials": []
}
```

* **`id`** (string): The session identifier (the `issuanceSessionId` returned at offer creation).
* **`issuerId`** (string): The issuer DID associated with the session.
* **`state`** (string): The current state of the issuance session (e.g. `OfferCreated`, `Completed`).
* **`credentialOfferUri`** (string): The offer URI tied to the session.
* **`createdAt`** / **`expiresAt`** (string): ISO 8601 timestamps for creation and expiry.
* **`userPin`** (string, optional): The PIN, when the session used a PIN-protected pre-authorized code.
* **`errorMessage`** (string, optional): Present only when the session ended in the `Error` state; omitted otherwise.
* **`issuedCredentials`** (array of strings): The credential configuration ids issued so far in this session (e.g. `EmployeeBadge@1.0:sd-jwt`). Empty until credentials are issued; fully populated once the session reaches `Completed`.

***

## 6. Direct Signing (optional)

When you need a signed credential without the offer/wallet flow — for example to embed a credential in another system or to issue server-to-server — sign it directly. The issuer signs the payload with its DID's key and returns the credential in compact serialized form.

```
POST /issuers/:issuerDid/credentials/sign
Authorization: Bearer <token>
Content-Type: application/json
```

### Request Body

* **`format`** (string, required): One of `sd-jwt-vc` or `jwt_vc_json`.
* **`payload`** (object, required): The credential claims. For SD-JWT VC, include a `vct` (verifiable credential type) value.
* **`subjectDid`** (string, optional): The DID of the credential subject, used as the subject identifier in the credential.
* **`disclosureFrame`** (object, optional): SD-JWT only. An object with an `_sd` array listing the claim keys that should be selectively disclosable.

**Example Request**

```json
{
  "format": "sd-jwt-vc",
  "payload": {
    "vct": "https://issuer.example.com/vct/employee-badge",
    "employee_id": "EMP-001",
    "full_name": "Ada Lovelace",
    "department": "Engineering"
  },
  "subjectDid": "did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRfGNUg9V8q8",
  "disclosureFrame": {
    "_sd": ["employee_id", "full_name", "department"]
  }
}
```

### Response Body

```json
{
  "credential": "eyJhbGciOiJFZERTQSJ9.eyJ2Y3QiOi...~WyJhIiwiZW1wbG95ZWVfaWQiXQ~",
  "format": "dc+sd-jwt"
}
```

* **`credential`** (string): The signed credential in compact serialized form (a compact JWT for `jwt_vc_json`, or an SD-JWT with appended disclosures for `sd-jwt-vc`).
* **`format`** (string): The canonical credential format identifier of the signed credential. This is **not** the request enum value — it is the format the credential is actually issued in: `dc+sd-jwt` for an SD-JWT VC (request `format: "sd-jwt-vc"`), and `jwt_vc` for a JWT-VC-JSON credential (request `format: "jwt_vc_json"`).

***

That is the whole flow: create an offer, present it as a QR or deep link, and let the wallet claim it — tracking progress through the issuance session. For one-off, non-interactive issuance, use the direct signing endpoint.


# Issuance Sessions

Every credential offer created by an issuer opens an **issuance session** that the platform tracks from the moment the offer is generated until the credential is delivered to a wallet (or the offer expires). Issuance sessions let you observe issuance progress, retrieve the offer URI again, and regenerate the QR code — without re-creating the offer.

All issuance-session endpoints require a valid Bearer token:

```bash
Authorization: Bearer <token>
```

The service validates the JWT against the configured JWKS endpoint; any valid token is accepted at this layer (see [Authentication](/develop/issuer#authentication)).

## How a session is created

A session is opened automatically when you create a credential offer — there is no separate create-session endpoint. The offer response returns an `issuanceSessionId`, which is the handle for every operation on this page.

**POST** `/issuers/:issuerDid/credential-offers`

```bash
POST /issuers/did:key:z6Mkk.../credential-offers
Authorization: Bearer <token>
Content-Type: application/json
```

### Request Body

```json
{
  "credentialConfigurationIds": ["EmployeeBadge@1.0:sd-jwt"],
  "requirePin": false,
  "issuanceMetadata": {
    "claimsByConfigurationId": {
      "EmployeeBadge@1.0:sd-jwt": {
        "employee_id": "EMP-001",
        "full_name": "Ada Lovelace",
        "department": "Engineering"
      }
    }
  }
}
```

### Response Body

```json
{
  "credentialOfferUri": "openid-credential-offer://?credential_offer=...",
  "issuanceSessionId": "d07d0df5-0d64-4f1a-9aa7-7e7f9c2a1d70",
  "credentialConfigurationIds": ["EmployeeBadge@1.0:sd-jwt"]
}
```

* **credentialOfferUri**: The `openid-credential-offer://` URI a wallet scans or follows to claim the credential.
* **issuanceSessionId**: The identifier of the session opened for this offer. Use it to track progress and to fetch the session's QR code.
* **credentialConfigurationIds**: The credential configuration ids the offer covers, each of the form `Name@Version:format`.
* **userPin**: Present only when `requirePin` is `true`; the PIN the holder must enter to complete the pre-authorized code flow.

Optional fields such as `userPin` are omitted from the JSON when they do not apply (for example, when `requirePin` is `false`) rather than returned as `null`. Keep the returned `issuanceSessionId` — it is the path parameter for the get-session and QR-code endpoints below.

## List sessions for an issuer

Returns all issuance sessions belonging to a single issuer DID, most useful for an operator dashboard that shows pending and completed offers. The `issuerId` query parameter filters sessions to one issuer; it is not checked against existing issuers, and omitting it returns all issuance sessions across all issuers.

**GET** `/issuance-sessions?issuerId=<issuerId>`

```bash
GET /issuance-sessions?issuerId=did:key:z6Mkk...
Authorization: Bearer <token>
```

### Response Body

```json
[
  {
    "id": "d07d0df5-0d64-4f1a-9aa7-7e7f9c2a1d70",
    "issuerId": "did:key:z6Mkk...",
    "state": "OfferCreated",
    "credentialOfferUri": "openid-credential-offer://?credential_offer=...",
    "createdAt": "2026-06-22T12:34:56.789Z",
    "expiresAt": "2026-06-22T12:49:56.789Z",
    "issuedCredentials": []
  }
]
```

* **issuerId**: The issuer DID to filter by, passed as the `issuerId` query parameter.

For a freshly created `OfferCreated` session, `issuedCredentials` is an empty array (no credentials have been issued yet), and the optional `userPin` and `errorMessage` fields are absent from the JSON because they do not apply.

## Get a single session

Reads one session by its identifier, returning its current state and all tracking fields.

**GET** `/issuance-sessions/:sessionId`

```bash
GET /issuance-sessions/d07d0df5-0d64-4f1a-9aa7-7e7f9c2a1d70
Authorization: Bearer <token>
```

### Response Body

```json
{
  "id": "d07d0df5-0d64-4f1a-9aa7-7e7f9c2a1d70",
  "issuerId": "did:key:z6Mkk...",
  "state": "Completed",
  "credentialOfferUri": "openid-credential-offer://?credential_offer=...",
  "createdAt": "2026-06-22T12:34:56.789Z",
  "expiresAt": "2026-06-22T12:49:56.789Z",
  "issuedCredentials": ["EmployeeBadge@1.0:sd-jwt"]
}
```

Optional fields are omitted when they do not apply: `userPin` appears only when the offer required a PIN, and `errorMessage` appears only when the session ended in the `Error` state.

**Session fields**

* **id**: Internal session identifier (the same value as `issuanceSessionId` from the offer response).
* **issuerId**: The issuer DID that owns the session.
* **state**: Current lifecycle state of the session (see below).
* **credentialOfferUri**: The credential offer URI tied to the session.
* **createdAt**: ISO 8601 timestamp of when the session (and offer) was created.
* **expiresAt**: ISO 8601 timestamp after which the offer can no longer be claimed.
* **userPin**: The PIN for the pre-authorized code flow, present only when the offer required one.
* **errorMessage**: A human-readable error message when the session ended in the `Error` state; otherwise absent.
* **issuedCredentials**: The credential configuration ids issued in this session (for example `EmployeeBadge@1.0:sd-jwt`). It is an empty array until issuance completes, and is filled in as credentials are issued (fully populated once the state reaches `Completed`).

## Session states

The `state` field advances through the OpenID4VCI pre-authorized code flow as the wallet interacts with the issuer:

* **OfferCreated**: The offer has been generated and the session is waiting for a wallet to retrieve it.
* **OfferUriRetrieved**: A wallet has fetched the offer payload from the offer URI.
* **AuthorizationInitiated** / **AuthorizationGranted**: Authorization steps preceding token issuance.
* **AccessTokenRequested** / **AccessTokenCreated**: The wallet has requested and received an access token to call the credential endpoint.
* **CredentialRequestReceived**: The wallet has submitted a credential request.
* **CredentialsPartiallyIssued**: At least one — but not all — of the offered credentials has been issued.
* **Completed**: All offered credentials have been issued; `issuedCredentials` lists their configuration ids.
* **Error**: Issuance failed; `errorMessage` describes what went wrong.

To track issuance progress, poll **GET** `/issuance-sessions/:sessionId` and watch the `state` field. A session is finished when it reaches `Completed` (success) or `Error` (failure), or when `expiresAt` has passed without completion.

## Get the session QR code

Returns a PNG QR code that encodes the session's credential offer URI. This is handy for re-displaying an offer for an existing session without creating a new one — for example, refreshing the QR shown in an operator panel.

**GET** `/issuance-sessions/:sessionId/qr-code`

```bash
GET /issuance-sessions/d07d0df5-0d64-4f1a-9aa7-7e7f9c2a1d70/qr-code
Authorization: Bearer <token>
Accept: image/png
```

### Response

* **Body**: A 400×400 PNG image (`Content-Type: image/png`) of the QR code.
* **Header `X-Credential-Offer-Uri`**: The `openid-credential-offer://` URI encoded in the QR code, so callers can copy the offer without decoding the image.

A wallet that scans this QR code is taken through the same flow as the original offer, and its progress is reflected in the same session. The endpoint always regenerates the image from the session's stored offer URI, so it stays valid for the life of the offer (until `expiresAt`).

<figure><img src="/files/euvQwqLt5KDV5BhtoHoV" alt="Issuance session QR code in the operator console"><figcaption><p>An issuance session and its QR code in the operator console (Issuance Sessions tab)</p></figcaption></figure>

## Notes

* Use the `issuanceSessionId` from the offer response as the `:sessionId` path parameter for the get-session and QR-code endpoints.
* If you need the offer URI and QR at creation time rather than after the fact, the credential-offer endpoint has a `qr-code` variant (`POST /issuers/:issuerDid/credential-offers/qr-code`) that returns the PNG directly with `X-Credential-Offer-Uri`, `X-Issuance-Session-Id`, and `X-Pin` headers. See the credential issuance flow documentation.


# Direct Credential Signing

Direct signing produces a signed credential in a single request, without any wallet involvement. Instead of creating a credential offer and waiting for a holder to claim it, you submit the claims and the issuer immediately returns the signed credential in compact serialized form. This is useful when the credential is bound to a passive subject (a product, a device, an asset) rather than to a wallet that can prove key possession, or when your application already has its own delivery channel for the resulting credential.

All direct-signing requests require a valid Bearer token, sent as `Authorization: Bearer <token>` (see [Authentication](/develop/issuer#authentication)).

***

## 1. Direct Signing vs an OpenID4VCI Offer

The platform supports two ways to issue a credential. Choose based on who the subject is and how the credential reaches its holder.

* **OpenID4VCI offer** (see [Issuing Credentials & Interacting with Wallets](/develop/issuer/credential-issuance-flow)): the issuer creates a credential offer, the recipient scans a QR code or follows an `openid-credential-offer://` deep link, and a wallet claims the credential. The wallet proves possession of its key, so the issued credential is cryptographically bound to the holder (a `cnf` / holder-binding claim). Use this for credentials issued to people who hold a wallet.
* **Direct signing** (this page): the issuer signs and returns the credential synchronously, with no wallet flow and no proof of possession. The subject is identified only by an optional `subjectDid`. Use this when the subject cannot run a wallet or perform a key-binding proof — for example a Digital Product Passport bound to a product DID — or when you need the raw credential string to store or transmit yourself.

Both paths sign with the same issuer DID and produce the same credential formats. Direct signing simply skips the offer-and-claim handshake.

***

## 2. Sign a Credential

**`POST /issuers/:issuerDid/credentials/sign`**

```
POST /issuers/:issuerDid/credentials/sign
Authorization: Bearer <token>
Content-Type: application/json
```

* **`issuerDid`** (path, required): The DID of the issuer that signs the credential. Signing material is resolved from this DID, so it must be a DID the service controls (a `did:web` or `did:key` created via the Agent / DID endpoints). If the issuer's signing material cannot be resolved, the request fails with HTTP 400 Bad Request.

### Request Body

* **`format`** (string, required): The credential format to produce. One of:
  * `"sd-jwt-vc"` — SD-JWT VC, supporting selective disclosure.
  * `"jwt_vc_json"` — JWT-VC-JSON, a W3C Verifiable Credential serialized as a JWT.
* **`payload`** (object, required): The credential claims. For `sd-jwt-vc`, this object **must** include a `vct` (Verifiable Credential Type) string. For `jwt_vc_json`, an optional `type` field (string or array of strings) sets the credential `type` values.
* **`subjectDid`** (string, optional): The DID of the credential subject. For `sd-jwt-vc` it is written as the `sub` claim; for `jwt_vc_json` it becomes `credentialSubject.id`. This identifies the subject only — it does **not** create holder binding, since direct signing involves no proof of key possession.
* **`disclosureFrame`** (object, optional): Selective-disclosure configuration for SD-JWT. An object with a single key:

  * **`_sd`** (array of strings, required, at least one entry): the claim keys that should be made selectively disclosable.

  `disclosureFrame` is only valid when `format` is `"sd-jwt-vc"`. Supplying it with `jwt_vc_json` is rejected with HTTP 400 Bad Request.

### Response Body

```json
{
  "credential": "eyJhbGciOiJFZERTQSJ9.eyJ2Y3QiOi...~WyJhIiwiZW1wbG95ZWVfaWQiXQ~",
  "format": "dc+sd-jwt"
}
```

* **`credential`** (string): The signed credential in compact serialized form. For SD-JWT this is the issuer-signed JWT followed by `~`-separated disclosures; for JWT-VC-JSON it is a compact JWT.
* **`format`** (string): The claim format of the returned credential — `"dc+sd-jwt"` for an SD-JWT VC, or `"jwt_vc"` for a JWT-VC-JSON. These are the DIF claim-format identifiers, which differ from the `format` value you send in the request (`"sd-jwt-vc"` / `"jwt_vc_json"`).

***

## 3. SD-JWT with Selective Disclosure

For `sd-jwt-vc`, the `payload` carries the claims and `disclosureFrame._sd` lists which of those claims the holder can later disclose individually. Claims not listed in `_sd` are always present in the credential; listed claims are hashed into the SD-JWT so that, at presentation time, the subject can reveal them one by one without exposing the rest.

The service requires a `vct` in the payload and stamps an `iat` (issued-at) timestamp for you; you normally do not include `iat` in the payload (a value you supply there would override the default).

### Example Request

```json
{
  "format": "sd-jwt-vc",
  "payload": {
    "vct": "https://issuer.example.com/vct/employee-badge",
    "employee_id": "EMP-001",
    "full_name": "Ada Lovelace",
    "department": "Engineering",
    "access_level": "L3"
  },
  "subjectDid": "did:key:z6Mk...",
  "disclosureFrame": {
    "_sd": ["employee_id", "full_name", "department", "access_level"]
  }
}
```

### Example Response

```json
{
  "credential": "eyJhbGciOiJFZERTQSJ9.eyJ2Y3QiOi...~WyJzYWx0IiwiZnVsbF9uYW1lIiwiQWRhIExvdmVsYWNlIl0~",
  "format": "dc+sd-jwt"
}
```

In this example, `vct` is fixed and present in every presentation, while `employee_id`, `full_name`, `department`, and `access_level` become selectively disclosable: a verifier asking only for `department` receives just that claim.

If `payload.vct` is missing or empty, the request is rejected with HTTP 400 Bad Request and the message `payload.vct is required for sd-jwt-vc format.`

***

## 4. JWT-VC-JSON

For `jwt_vc_json`, the credential is built as a W3C Verifiable Credential and serialized as a JWT, signed with the issuer DID using EdDSA. The `type` field in `payload` sets the credential `type` array; `VerifiableCredential` is always included automatically. The remaining payload fields become `credentialSubject` claims, and `subjectDid` (if supplied) becomes `credentialSubject.id`. A reserved `id` key inside `payload` is not copied into the subject claims.

`disclosureFrame` is not applicable to this format and must be omitted.

### Example Request

```json
{
  "format": "jwt_vc_json",
  "payload": {
    "type": ["VerifiableCredential", "EmploymentCredential"],
    "employee_id": "EMP-001",
    "full_name": "Ada Lovelace",
    "department": "Engineering"
  },
  "subjectDid": "did:key:z6Mk..."
}
```

### Example Response

```json
{
  "credential": "eyJhbGciOiJFZERTQSJ9.eyJ2YyI6ey...",
  "format": "jwt_vc"
}
```

***

## 5. Errors

The endpoint validates the request body and the signing context, returning HTTP 400 Bad Request in these cases:

* The body fails validation (for example an unknown `format`, a missing `payload`, or a `disclosureFrame._sd` that is not a non-empty array of strings).
* `format` is `"sd-jwt-vc"` but `payload.vct` is missing or empty.
* `format` is `"jwt_vc_json"` but `disclosureFrame` was supplied (`disclosureFrame is only supported for sd-jwt-vc format.`).
* The signing material for the issuer DID cannot be resolved (the DID is unknown to the service or has no usable key).

Errors follow the global error shape: `{ statusCode, message, path, timestamp }`.

***

## Security

Direct signing produces a credential with no holder-binding proof, so the issuer is fully responsible for the correctness of the claims and the subject it names. Protect this endpoint accordingly: it requires a valid OIDC JWT Bearer token, and it should only be called by trusted backend systems that have already validated the data they are signing into a credential.


# Security Considerations

The sections below describe how the Issuer Service protects its API, the credentials it issues, and the signing keys it manages.

1. **API Authentication and Authorization**:
   * **OIDC JWT Bearer tokens**: Every non-public endpoint requires an OIDC-issued JWT presented as `Authorization: Bearer <token>`. Tokens are validated against the JWKS endpoint configured in `OIDC_JWKS_URL`: the signature is verified with `RS256`, the signing key is selected by matching the token's `kid` against the JWKS, and expiration is enforced. Fetched signing keys are cached for roughly 10 minutes (with a request limit of 10 JWKS fetches per minute) to limit round-trips to the identity provider. A request with a missing, malformed, or expired token is rejected with `401 Unauthorized`.
   * **Role-based access**: The `admin` role is additionally enforced on agent management routes (`/agent/*` — DID, issuer, verifier and holder management) and on cloud-wallet routes (`/holders/:holderId/*`). For these routes, the service inspects the token's realm roles (`realm_access.roles`) and client roles (`resource_access`) and rejects any caller without the `admin` role with `403 Forbidden`. Cloud-wallet routes additionally enforce ownership, so a caller can only access the holders they created. The remaining Issuer endpoints (schema management, credential offers, issuance sessions and direct signing) and the Verifier endpoints require only a valid token — they carry no role requirement. Restrict who can obtain tokens carrying the `admin` role in your identity provider.
   * **Development bypass**: Setting `AUTH_DISABLED=true` disables token validation and treats every request as an authenticated `admin` user. This is a local-development convenience only and must never be enabled in production.
2. **Public Endpoints**: A number of endpoints are intentionally unauthenticated: `GET /` (hello), `GET /version`, and the hosted DID document routes (`GET /:uuid/did.json` and `GET /:uuid/.well-known/did.json`). These expose only public information — a service identifier, a version string, and DID documents that are meant to be world-readable so that verifiers can resolve issuer keys. Also public are the Swagger docs (`GET /api-docs`, `GET /api-docs-json`) and the wallet-facing OpenID4VCI/OpenID4VP protocol endpoints mounted under the `ISSUER_BASE_URL`/`VERIFIER_BASE_URL` paths (credential-issuer metadata, token, credential, and authorization-request/response endpoints), which are public by protocol design.
3. **Transport Security**: Always use HTTPS to protect tokens and credentials in transit. The OpenID4VCI issuance flow requires HTTPS in production. The `ALLOW_INSECURE_HTTP=true` flag permits plain HTTP for local development only and must be left unset (or `false`) in any deployed environment.
4. **Credential Integrity**: Issued credentials are cryptographically signed with the issuer's `Ed25519` key using the `EdDSA` algorithm. The signature binds the claims to the issuing DID, so any tampering with a credential — for example with an `EmployeeBadge@1.0:sd-jwt` credential — invalidates the signature and causes verification to fail. For SD-JWT VC credentials, selective disclosure lets a holder reveal only chosen claims while the issuer signature still covers the full set.
5. **Rate Limiting**: A global throttle of 60 requests per 60 seconds is applied to guard against brute-force and abuse. DID document serving is exempt so that verifiers can always resolve issuer keys. Requests exceeding the limit receive `429 Too Many Requests`.
6. **Access Controls and Monitoring**: In addition to the built-in token validation and role-based access control, operators should apply standard hardening at the network layer: IP allow-listing, firewalls, and audit logging to track sensitive operations and surface suspicious activity.
7. **Key Management**: Issuer signing keys are `Ed25519` keys held in an internal, database-backed key-management system; private key material is never exposed through the API. Automated key rotation is not available today; it is a planned future capability (see [Future Enhancements](/develop/issuer/future-enhancements)).


# Error Handling and Troubleshooting

The Issuer Service uses standard HTTP status codes and JSON error responses. A global error filter normalizes every failure into a consistent JSON body, so clients can rely on the same shape across all endpoints.

**Common Status Codes**:

* **400 Bad Request**: Invalid request parameters or missing fields (for example, a credential schema whose `name` contains spaces, or a credential offer with an empty `credentialConfigurationIds` array). The global validation pipe also rejects any JSON property that is not part of the endpoint's schema, with a message like `property X should not exist`.
* **401 Unauthorized**: A missing, malformed, or expired OIDC JWT Bearer token, or a token whose signature cannot be verified against the configured JWKS endpoint.
* **403 Forbidden**: The token is valid but lacks the required `admin` role (accepted from `realm_access.roles` or a `resource_access` client-role entry) on a route that enforces it, or it does not grant ownership of the requested holder.
* **404 Not Found**: A non-existent schema ID (`GET /schemas/:schemaId`) or a request for a hosted DID document that does not exist. Unknown issuer, session, or credential-offer identifiers currently surface as `500` rather than `404`.
* **409 Conflict**: A schema with the same `name` and `version` already exists (`POST /schemas`).
* **410 Gone**: The requested schema version has been superseded by a newer version. Retrieve it by passing `includeInactive=true` on the schema endpoint.
* **429 Too Many Requests**: The global rate limit (60 requests per 60 seconds) has been exceeded. DID document serving is exempt from this limit.
* **500 Internal Server Error**: Unexpected server issues.

**Example Error Response**:

```json
{
  "statusCode": 410,
  "message": "Schema version is superseded.",
  "error": "Gone",
  "path": "/schemas/EmployeeBadge@1.0",
  "timestamp": "2026-06-22T10:15:30.000Z"
}
```

Error bodies always include `statusCode`, `message`, `path`, and `timestamp`; responses produced by standard HTTP exceptions additionally include an `error` label (for example, `"Gone"` for 410 or `"Conflict"` for 409).

**Troubleshooting Tips**:

* Validate request payloads against your credential schema definitions before sending. For SD-JWT credentials, ensure the `vct` claim is present in the signing payload.
* Ensure the request carries a current, unexpired OIDC JWT Bearer token in the `Authorization: Bearer <token>` header. The `admin` role is required only on the agent/DID/issuer/verifier/holder management routes (`/agent/*`) and the cloud-wallet routes (`/holders/:holderId/*`).
* For `401` errors, confirm the JWKS endpoint (`OIDC_JWKS_URL`) is reachable, that the token's `kid` header matches a key published by that endpoint, and that the token has not expired.
* For `403` errors on a management or holder route, confirm the token includes the `admin` role in `realm_access.roles` or in a `resource_access` client-role entry, and that the holder you are addressing belongs to the authenticated user.
* For `410` errors, the schema version is superseded. Use `includeInactive=true` to read the old version, or reference the current active version returned by `GET /schemas/:schemaName/versions`.
* Verify database connectivity (`DATABASE_URL`) — the service stores schemas, sessions, signing keys, and hosted DID documents in PostgreSQL.
* Check service logs for detailed error messages. Server-side failures (`5xx`) are logged with a stack trace; client errors (`4xx`) are logged as warnings.

If issues persist, review your environment configuration (`DATABASE_URL`, `ISSUER_BASE_URL`, `OIDC_JWKS_URL`), your schema definitions, and network connectivity between the service and its OIDC provider.


# Future Enhancements

Planned features aim to increase flexibility, security, and privacy. The items below are not yet implemented; they describe the direction of the Issuer Service.

* **Key Rotation and DID Updates**: Rotate signing keys without downtime and update hosted `did:web` documents to publish new verification methods, so that previously issued credentials remain verifiable while new credentials are signed with fresh keys. Today, signing keys are `Ed25519` keys managed automatically by the service and are not rotatable through the API.
* **Zero-Knowledge Proof (ZKP) Support**: Add unlinkable predicate proofs (for example, proving "is over 18" without revealing a date of birth) on top of the selective disclosure already available, further reducing the data a holder must share and limiting correlation across presentations.


# References and Standards

The Issuer Service is built on open, standards-based specifications so that the credentials it issues remain interoperable with third-party wallets and verifiers. The references below cover the credential data models, identifier methods, issuance and presentation protocols, and query languages the service implements.

## Credential data models and formats

* **W3C Verifiable Credentials Data Model**: <https://www.w3.org/TR/vc-data-model/> The underlying data model for verifiable credentials. The service issues **JWT-VC-JSON** credentials (`jwt-vc-json` / `jwt_vc_json`) that follow this model.
* **SD-JWT VC (Selective Disclosure JWT Verifiable Credentials)**: <https://datatracker.ietf.org/doc/draft-ietf-oauth-sd-jwt-vc/> The format used for **SD-JWT VC** credentials (`vc+sd-jwt` / `sd-jwt-vc`). Each SD-JWT VC carries a `vct` (Verifiable Credential Type) and supports selective disclosure of individual claims. For example, a schema named `EmployeeBadge` at version `1.0` yields the credential configuration `EmployeeBadge@1.0:sd-jwt`.
* **Selective Disclosure for JWTs (SD-JWT)**: <https://datatracker.ietf.org/doc/draft-ietf-oauth-selective-disclosure-jwt/> The underlying mechanism that lets a holder reveal only selected claims when presenting an SD-JWT VC. A schema's `disclosureFrame` declares which claims are selectively disclosable.

## Identifiers

* **DID Core Specification**: <https://www.w3.org/TR/did-core/> The base specification for Decentralized Identifiers and DID documents.
* **DID Method: `did:web`**: <https://w3c-ccg.github.io/did-method-web/> HTTPS-resolvable DIDs. The service hosts the DID document itself and serves it at `/{uuid}/did.json` (also at `/{uuid}/.well-known/did.json`).
* **DID Method: `did:key`**: <https://w3c-ccg.github.io/did-method-key/> Self-contained DIDs (Ed25519) that encode the public key directly in the identifier, requiring no network lookup.

## Issuance and presentation protocols

* **OpenID for Verifiable Credential Issuance (OpenID4VCI)**: <https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html> The protocol the Issuer Service uses to deliver credentials to wallets. The service implements the pre-authorized code flow with an optional PIN.
* **OpenID for Verifiable Presentations (OpenID4VP)**: <https://openid.net/specs/openid-4-verifiable-presentations-1_0.html> The protocol used to request and receive verifiable presentations from wallets when a credential is later presented to a verifier. Authorization requests can target versions `v1`, `v1.draft21`, or `v1.draft24`.

## Presentation query languages

* **DIF Presentation Exchange v2**: <https://identity.foundation/presentation-exchange/> Describes the credentials a verifier requires in a presentation request.
* **DCQL (Digital Credentials Query Language)**: <https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#name-digital-credentials-query-l> The OpenID4VP-native query language, supported as an alternative to DIF Presentation Exchange.

## Ecosystem context

For the `did:empe` method and the EMPE blockchain, see [Terminology and Concepts](/develop/issuer/terminology-and-concepts).


# MCP Server Integration

The [Empe MCP Server](https://github.com/empe-io/mcp-server) is an external companion project, maintained in its own repository, that lets AI clients such as Claude drive the Issuer Service through natural language. It implements the Model Context Protocol (MCP): an MCP server exposes tools and data sources so that an AI client can call them on your behalf. It is not part of the Issuer Service deployment — you run it separately and point it at your issuer instance.

## Issuer-Related Tools

The MCP server registers these tools for working with an issuer:

### Schema Management

* `create_schema` - Create new credential schemas through natural language
* `get_all_schemas` - List all available schemas
* `get_schema_by_id` - Retrieve specific schema details
* `delete_schema` - Remove unused schemas
* `schema_exists_by_type` - Check if a schema type already exists
* `get_latest_schema_by_type` - Get the most recent version of a schema

### Credential Issuance

* `create_offering` - Generate credential offerings with QR codes

Example prompts:

```
"Create a digital ticket schema for my tech conference with fields for
event name, date, seat number, and ticket ID."

"Show me all the credential schemas available in the system."

"Generate a QR code for claiming a concert ticket for the Summer Jazz Festival."
```

## Setup

1. **Deploy an Issuer Service** via [One-Click Deployment](https://oneclick.empe.io/)
2. **Configure the MCP server** with your issuer's endpoint and credentials:

```
ISSUER_URL=https://your-issuer-instance.example.com
ISSUER_API_KEY=your-issuer-api-key
```

3. **Connect the MCP server** to your AI client (like Claude)

The MCP server targets One-Click deployments; for detailed setup instructions and version compatibility, see the [MCP Server repository](https://github.com/empe-io/mcp-server).

## Security Considerations

When using the MCP server with production issuer services:

* Use dedicated test environments for experimentation
* Implement proper access controls for the MCP server
* Monitor AI-generated schema and credential operations
* Validate all AI-created schemas before production use


# FAQ

Common questions about issuing credentials with the Issuer Service, answered against the current API behavior.

**Q: How can I restrict who is able to claim or use a credential I issue?**\
A: There are two complementary mechanisms. First, you can bind a credential to a specific subject by setting `subjectDid` when signing directly through **POST /issuers/:issuerDid/credentials/sign** — for SD-JWT VC the value is written to the `sub` claim, and for JWT-VC-JSON it becomes `credentialSubject.id`. Second, you can require a PIN on a credential offer by setting `requirePin: true` when calling **POST /issuers/:issuerDid/credential-offers**; the issuance response then returns a `userPin` that the holder must enter to complete the pre-authorized flow, so only someone who received the PIN out of band can claim the credential. There is no offering mode that targets a recipient DID and forces the wallet to prove ownership of it before claiming.

**Q: Will updating a schema affect previously issued credentials?**\
A: No. Issued credentials are self-contained and remain valid regardless of later schema changes. When you create a schema whose name already has an active version, the previous version is marked `superseded` and the new one becomes active. Superseded versions are hidden from the default listings, and **GET /schemas/:schemaId** returns `410 Gone` for them — but you can still retrieve them by passing `includeInactive=true`, or list every version with **GET /schemas/:schemaName/versions**.

**Q: How do wallets retrieve credentials?**\
A: Issuance uses the OpenID4VCI pre-authorized code flow. You create an offer with **POST /issuers/:issuerDid/credential-offers** (or the `/qr-code` variant), which returns an `openid-credential-offer://...` URI. The holder scans the QR code or opens the offer link, enters the PIN if one is required, and the wallet then completes the pre-authorized exchange and receives the credential automatically — there are no separate authorization or token endpoints for you to call.

**Q: Which credential formats and DID methods are supported?**\
A: The Issuer Service issues credentials as **SD-JWT VC** (`vc+sd-jwt` / `sd-jwt-vc`) and **JWT-VC-JSON** (`jwt-vc-json` / `jwt_vc_json`). Issuer DIDs use the **`did:web`** and **`did:key`** methods; `did:web` documents are hosted and served by the service itself over HTTPS.

**Q: Does the Issuer Service support selective disclosure?**\
A: Yes, today, via SD-JWT VC. Declare which claims are selectively disclosable with `disclosureFrame` when creating a schema, or pass a `disclosureFrame` of the form `{ "_sd": ["..."] }` when signing directly through **POST /issuers/:issuerDid/credentials/sign**. For example, a schema named `EmployeeBadge` (version `1.0`) could mark `employee_id`, `full_name` and `department` as disclosable so the holder can later reveal only the claims a verifier asks for during a presentation. Selective disclosure applies to SD-JWT VC only; it is not available for JWT-VC-JSON.

**Q: Can I rotate signing keys?**\
A: Not yet. Keys are managed automatically by the Issuer Service. Configurable key rotation is planned for a future release.

**Q: Will the Issuer Service support additional privacy features?**\
A: Selective disclosure is already available through SD-JWT VC (see above). Zero-knowledge proofs (ZKPs) are being evaluated as a future enhancement for privacy-preserving credential presentations.


# API Reference


# Token

Exchange authorization code for access token\
\
Validates the authorization code and creates a JWT access token for the holder
------------------------------------------------------------------------------

> Creates a new access token using an authorization code

```json
{"openapi":"3.0.0","info":{"title":"issuer-service","version":"1.0.0-rc2"},"servers":[{"url":"/"}],"security":[],"paths":{"/api/v1/tokens":{"post":{"operationId":"CreateAccessToken","responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccessTokenResponseDto"}}}},"400":{"description":"Bad Request - Invalid or missing authorization code","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Not Found - Authorization request not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"500":{"description":"Server Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"description":"Creates a new access token using an authorization code","summary":"Exchange authorization code for access token\n\nValidates the authorization code and creates a JWT access token for the holder","tags":["Token"],"parameters":[],"requestBody":{"description":"Request containing the authorization code","required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAccessTokenRequestDto","description":"Request containing the authorization code"}}}}}}},"components":{"schemas":{"AccessTokenResponseDto":{"description":"Response DTO for access token operations","properties":{"access_token":{"type":"string","description":"JWT access token"},"token_type":{"type":"string","description":"Token type (always \"Bearer\")"},"expires_in":{"type":"number","format":"double","description":"Token expiration time in seconds","minimum":0}},"required":["access_token","token_type","expires_in"],"type":"object","additionalProperties":false},"ErrorResponseDto":{"description":"Extended error response with HTTP details","properties":{"name":{"type":"string","description":"Error name"},"message":{"type":"string","description":"Error message"},"code":{"type":"number","format":"double","description":"Error code"},"statusCode":{"type":"number","format":"double","description":"HTTP status code"},"httpCode":{"type":"number","format":"double","description":"HTTP code"},"method":{"type":"string","description":"HTTP method"},"path":{"type":"string","description":"Request path"},"stack":{"type":"string","description":"Stack trace (tylko w środowisku deweloperskim)"}},"required":["name","message","code","statusCode","httpCode","method","path"],"type":"object","additionalProperties":false},"CreateAccessTokenRequestDto":{"description":"Request DTO for creating a new access token","properties":{"authorization_code":{"type":"string","description":"Authorization code received from the authorization request\nThe code must be a non-empty string in format of [A-Za-z0-9-_] with length between 32 and 128 characters"}},"required":["authorization_code"],"type":"object","additionalProperties":false}}}}
```


# Schema

## List all credential schemas

> Retrieves all credential schemas

```json
{"openapi":"3.0.0","info":{"title":"issuer-service","version":"1.0.0-rc2"},"servers":[{"url":"/"}],"security":[],"paths":{"/api/v1/schemas":{"get":{"operationId":"GetSchemaCollection","responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/SchemaResponseDto"},"type":"array"}}}},"400":{"description":"Bad Request - Invalid query parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"description":"Retrieves all credential schemas","summary":"List all credential schemas","tags":["Schema"],"parameters":[{"description":"Filter by schema type","in":"query","name":"type","required":false,"schema":{"type":"string"}},{"description":"Filter by schema name","in":"query","name":"name","required":false,"schema":{"type":"string"}},{"description":"Filter by schema version","in":"query","name":"version","required":false,"schema":{"format":"double","type":"number"}},{"description":"Sort field","in":"query","name":"sortBy","required":false,"schema":{"type":"string","enum":["type","name","version"]}},{"description":"Sort order","in":"query","name":"sortOrder","required":false,"schema":{"$ref":"#/components/schemas/SortOrder"}}]}}},"components":{"schemas":{"SchemaResponseDto":{"description":"Response DTO for schema operations","properties":{"id":{"type":"string","description":"Unique identifier of the schema"},"type":{"type":"string","description":"Schema type"},"name":{"type":"string","description":"Schema name"},"version":{"type":"number","format":"double","description":"Schema version"},"schemaUri":{"type":"string","description":"Schema URI"},"schemaBody":{"description":"Schema body"}},"required":["id","type","name","version"],"type":"object","additionalProperties":false},"ErrorResponseDto":{"description":"Extended error response with HTTP details","properties":{"name":{"type":"string","description":"Error name"},"message":{"type":"string","description":"Error message"},"code":{"type":"number","format":"double","description":"Error code"},"statusCode":{"type":"number","format":"double","description":"HTTP status code"},"httpCode":{"type":"number","format":"double","description":"HTTP code"},"method":{"type":"string","description":"HTTP method"},"path":{"type":"string","description":"Request path"},"stack":{"type":"string","description":"Stack trace (tylko w środowisku deweloperskim)"}},"required":["name","message","code","statusCode","httpCode","method","path"],"type":"object","additionalProperties":false},"SortOrder":{"enum":["ASC","DESC"],"type":"string"}}}}
```

## Create a new credential schema

> Creates a new credential schema

```json
{"openapi":"3.0.0","info":{"title":"issuer-service","version":"1.0.0-rc2"},"servers":[{"url":"/"}],"security":[{"clientSecret":[]}],"components":{"securitySchemes":{"clientSecret":{"type":"apiKey","name":"x-client-secret","in":"header"}},"schemas":{"SchemaResponseDto":{"description":"Response DTO for schema operations","properties":{"id":{"type":"string","description":"Unique identifier of the schema"},"type":{"type":"string","description":"Schema type"},"name":{"type":"string","description":"Schema name"},"version":{"type":"number","format":"double","description":"Schema version"},"schemaUri":{"type":"string","description":"Schema URI"},"schemaBody":{"description":"Schema body"}},"required":["id","type","name","version"],"type":"object","additionalProperties":false},"ErrorResponseDto":{"description":"Extended error response with HTTP details","properties":{"name":{"type":"string","description":"Error name"},"message":{"type":"string","description":"Error message"},"code":{"type":"number","format":"double","description":"Error code"},"statusCode":{"type":"number","format":"double","description":"HTTP status code"},"httpCode":{"type":"number","format":"double","description":"HTTP code"},"method":{"type":"string","description":"HTTP method"},"path":{"type":"string","description":"Request path"},"stack":{"type":"string","description":"Stack trace (tylko w środowisku deweloperskim)"}},"required":["name","message","code","statusCode","httpCode","method","path"],"type":"object","additionalProperties":false},"CreateSchemaRequestDto":{"description":"Request DTO for creating a new schema","properties":{"name":{"type":"string","description":"Name of the schema"},"type":{"type":"string","description":"Type of the schema"},"version":{"type":"number","format":"double","description":"Schema version"},"credentialSubject":{"$ref":"#/components/schemas/CredentialSubject","description":"Credential subject definition\nSchema fields that define the structure of the credential"}},"required":["name","type","credentialSubject"],"type":"object","additionalProperties":false},"CredentialSubject":{"$ref":"#/components/schemas/Record_string.unknown_"},"Record_string.unknown_":{"properties":{},"additionalProperties":{},"type":"object","description":"Construct a type with a set of properties K of type T"}}},"paths":{"/api/v1/schemas":{"post":{"operationId":"CreateSchema","responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SchemaResponseDto"}}}},"400":{"description":"Bad Request - Invalid input parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"401":{"description":"Unauthorized - Invalid client secret","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"Conflict - Schema already exists","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"description":"Creates a new credential schema","summary":"Create a new credential schema","tags":["Schema"],"parameters":[],"requestBody":{"description":"Schema definition including name, type, version and credential subject","required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSchemaRequestDto","description":"Schema definition including name, type, version and credential subject"}}}}}}}}
```

## Get a schema by ID

> Retrieves a specific credential schema by ID

```json
{"openapi":"3.0.0","info":{"title":"issuer-service","version":"1.0.0-rc2"},"servers":[{"url":"/"}],"security":[],"paths":{"/api/v1/schemas/{id}":{"get":{"operationId":"GetSchemaById","responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"additionalProperties":false,"type":"object"}}}},"400":{"description":"Bad Request - Invalid schema ID format","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Not Found - Schema with given ID does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"description":"Retrieves a specific credential schema by ID","summary":"Get a schema by ID","tags":["Schema"],"parameters":[{"description":"Schema ID to retrieve","in":"path","name":"id","required":true,"schema":{"type":"string"}}]}}},"components":{"schemas":{"ErrorResponseDto":{"description":"Extended error response with HTTP details","properties":{"name":{"type":"string","description":"Error name"},"message":{"type":"string","description":"Error message"},"code":{"type":"number","format":"double","description":"Error code"},"statusCode":{"type":"number","format":"double","description":"HTTP status code"},"httpCode":{"type":"number","format":"double","description":"HTTP code"},"method":{"type":"string","description":"HTTP method"},"path":{"type":"string","description":"Request path"},"stack":{"type":"string","description":"Stack trace (tylko w środowisku deweloperskim)"}},"required":["name","message","code","statusCode","httpCode","method","path"],"type":"object","additionalProperties":false}}}}
```

## Delete a schema by ID

> Deletes a specific credential schema

```json
{"openapi":"3.0.0","info":{"title":"issuer-service","version":"1.0.0-rc2"},"servers":[{"url":"/"}],"security":[{"clientSecret":[]}],"components":{"securitySchemes":{"clientSecret":{"type":"apiKey","name":"x-client-secret","in":"header"}},"schemas":{"ErrorResponseDto":{"description":"Extended error response with HTTP details","properties":{"name":{"type":"string","description":"Error name"},"message":{"type":"string","description":"Error message"},"code":{"type":"number","format":"double","description":"Error code"},"statusCode":{"type":"number","format":"double","description":"HTTP status code"},"httpCode":{"type":"number","format":"double","description":"HTTP code"},"method":{"type":"string","description":"HTTP method"},"path":{"type":"string","description":"Request path"},"stack":{"type":"string","description":"Stack trace (tylko w środowisku deweloperskim)"}},"required":["name","message","code","statusCode","httpCode","method","path"],"type":"object","additionalProperties":false}}},"paths":{"/api/v1/schemas/{id}":{"delete":{"operationId":"DeleteSchema","responses":{"204":{"description":"No Content"},"400":{"description":"Bad Request - Invalid schema ID format","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"401":{"description":"Unauthorized - Invalid client secret","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Not Found - Schema with specified ID not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"Conflict - Schema is in use by credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"description":"Deletes a specific credential schema","summary":"Delete a schema by ID","tags":["Schema"],"parameters":[{"description":"Schema ID to delete","in":"path","name":"id","required":true,"schema":{"type":"string"}}]}}}}
```


# Offering

## List all credential offerings

> Retrieves all available offerings

```json
{"openapi":"3.0.0","info":{"title":"issuer-service","version":"1.0.0-rc2"},"servers":[{"url":"/"}],"security":[],"paths":{"/api/v1/offerings":{"get":{"operationId":"GetOfferings","responses":{"200":{"description":"A list of all offerings in the system","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/Offering"},"type":"array"}}}}},"description":"Retrieves all available offerings","summary":"List all credential offerings","tags":["Offering"],"parameters":[]}}},"components":{"schemas":{"Offering":{"properties":{"id":{"type":"string"},"credential_issuer":{"type":"string"},"credential_configuration_ids":{"items":{"type":"string"},"type":"array"},"display":{"properties":{"locale":{"type":"string"},"name":{"type":"string"}},"required":["locale","name"],"type":"object"},"grants":{"properties":{"authorization_code":{"properties":{"issuer_state":{"type":"string"}},"required":["issuer_state"],"type":"object"}},"required":["authorization_code"],"type":"object"},"credential_subject":{"$ref":"#/components/schemas/CredentialSubject"},"recipient":{"type":"string"},"redemption_limit":{"type":"number","format":"double","nullable":true},"expires_at":{"type":"string","format":"date-time","nullable":true},"redemptions":{"items":{"$ref":"#/components/schemas/OfferingRedemption"},"type":"array"}},"required":["id","credential_issuer","credential_configuration_ids","display","grants","credential_subject","redemptions"],"type":"object","additionalProperties":false},"CredentialSubject":{"$ref":"#/components/schemas/Record_string.unknown_"},"Record_string.unknown_":{"properties":{},"additionalProperties":{},"type":"object","description":"Construct a type with a set of properties K of type T"},"OfferingRedemption":{"description":"**OfferingRedemption**\n\nOne row = one **successful claim** of an `Offering` by a holder.\n\n* Maintains a full audit trail – even if the underlying `Credential`\n  is later deleted, the redemption record (with `credential_id = NULL`)\n  still proves the claim happened (`redeemed_at`, `recipient_did`).\n* The composite `UNIQUE(offering_id, credential_id)` block-inserts\n  double spending of the same credential, while\n  `IDX_redemption_offering_id` lets us quickly **count** redemptions in\n  a transaction and enforce `Offering.redemption_limit`.\n* Acts as the join table that links `Offering` ⇢ `Credential`, so the\n  business layer can fetch a credential’s issuing context or check how\n  many claims an offering has left.","properties":{"id":{"type":"string"},"offering_id":{"type":"string"},"credential_id":{"type":"string","nullable":true},"recipient_did":{"type":"string"},"redeemed_at":{"type":"string","format":"date-time"},"offering":{"$ref":"#/components/schemas/Offering"},"credential":{"allOf":[{"$ref":"#/components/schemas/Credential"}],"nullable":true}},"required":["id","offering_id","credential_id","recipient_did","redeemed_at","offering","credential"],"type":"object","additionalProperties":false},"Credential":{"description":"**Credential**\n\nRepresents a **single Verifiable Credential issued by the issuer-service**.\n\n- Each row is one credential that has already been signed and issued.\n- The `vc` column stores the **entire** `VerifiableCredential` object\n  (serialised JSON-JWT or JSON-LD).\n  A TypeORM transformer seamlessly converts between the raw JSON string\n  in the database and the `VerifiableCredential` class from `@empe/identity`.","properties":{"id":{"type":"string"},"configuration_id":{"type":"string"},"configuration":{"$ref":"#/components/schemas/CredentialConfiguration"},"vc":{"$ref":"#/components/schemas/VerifiableCredential","description":"Fully-serialised Verifiable Credential (includes `issuedAt`, `issuer`, `credentialSubject`, etc.)."},"revoked":{"type":"boolean","default":false}},"required":["id","configuration_id","configuration","vc"],"type":"object","additionalProperties":false},"CredentialConfiguration":{"properties":{"id":{"type":"string"},"name":{"type":"string","default":""},"type":{"type":"string","default":""},"version":{"type":"number","format":"double","default":1},"schemaUri":{"type":"string"},"schemaBody":{"additionalProperties":false,"type":"object"}},"required":["id","schemaUri"],"type":"object","additionalProperties":false},"VerifiableCredential":{"$ref":"#/components/schemas/VC","description":"Represents a Verifiable Credential (VC) that follows the W3C Verifiable Credentials Data Model.\n\nA Verifiable Credential is a set of tamper-evident claims and metadata that cryptographically proves\nwho issued it, when it was issued, when it expires (if applicable), and provides a secure way\nto express claims about a subject (a person, organization, or thing).\n\nKey features:\n- Tamper-evident: Any changes to the credential after issuance can be detected\n- Cryptographically verified: Contains proofs that can verify the issuer and integrity\n- Privacy-respecting: Supports selective disclosure and zero-knowledge proofs\n- Machine-verifiable: Can be automatically verified without contacting the issuer\n\nThis implementation supports:\n- JSON serialization for storage and transmission\n- JWT serialization for compatibility with existing JWT infrastructures\n- Schema validation to ensure credential data conforms to defined structures\n- Cryptographic proof generation and verification"},"VC":{"$ref":"#/components/schemas/VCv1_1","description":"Current version of the Verifiable Credential format used in the implementation"},"VCv1_1":{"properties":{"evidence":{"additionalProperties":false,"type":"object","description":"Additional supporting information for the claims in the credential"},"credentialStatus":{"items":{"$ref":"#/components/schemas/CredentialStatus"},"type":"array","description":"Credential status information, such as revocation or suspension"},"credentialSchema":{"$ref":"#/components/schemas/CredentialSchema","description":"Schema that defines the structure of the credential subject"},"expirationDate":{"type":"string","description":"Date and time when the credential expires"},"proof":{"$ref":"#/components/schemas/Proof","description":"Cryptographic proof that can be used to detect tampering and verify the issuer"},"credentialSubject":{"anyOf":[{"$ref":"#/components/schemas/CredentialSubject"},{"items":{"$ref":"#/components/schemas/CredentialSubject"},"type":"array"}],"description":"Claims about the subject of the credential"},"issuanceDate":{"type":"string","description":"Date and time when the credential was issued"},"issuer":{"$ref":"#/components/schemas/Issuer","description":"Entity that issued the credential"},"id":{"$ref":"#/components/schemas/URI","description":"Unique identifier for the credential"},"type":{"items":{"type":"string"},"type":"array","description":"Credential type information that defines what the credential represents"},"@context":{"anyOf":[{"items":{"$ref":"#/components/schemas/URI"},"type":"array"},{"$ref":"#/components/schemas/URI"}],"description":"JSON-LD context that defines the vocabulary of the credential"}},"required":["credentialSubject","issuanceDate","issuer","type","@context"],"type":"object","description":"Verifiable Credential format following W3C VC Data Model 1.1 specification."},"CredentialStatus":{"description":"A single entry in a bitstring-based status list, per VC-DATA-MODEL-2.0","properties":{"id":{"$ref":"#/components/schemas/URI"},"type":{"type":"string","enum":["BitstringStatusListEntry"],"nullable":false},"statusPurpose":{"anyOf":[{"$ref":"#/components/schemas/StatusPurpose"},{"type":"string"}]},"statusListIndex":{"type":"number","format":"double"},"statusListCredential":{"$ref":"#/components/schemas/URI"},"statusSize":{"type":"number","format":"double"},"statusMessage":{"items":{"properties":{"message":{"type":"string"},"status":{"type":"string"}},"additionalProperties":{},"required":["message","status"],"type":"object"},"type":"array"}},"required":["type","statusPurpose","statusListIndex","statusListCredential"],"type":"object","additionalProperties":false},"URI":{"type":"string"},"StatusPurpose":{"enum":["refresh","revocation","suspension","message"],"type":"string"},"CredentialSchema":{"properties":{"type":{"type":"string","enum":["JsonSchemaValidator2018"],"nullable":false,"description":"The type of schema validator, currently only supporting JsonSchemaValidator2018"},"id":{"$ref":"#/components/schemas/URI","description":"The identifier for the schema, typically a URI pointing to the schema definition"}},"required":["type","id"],"type":"object","description":"Represents a credential schema reference that defines the structure of claims in a credential.\nFollowing W3C VC Data Model, this provides the means to validate credential contents."},"Proof":{"properties":{"challange":{"type":"string"},"nonce":{"type":"string"},"jws":{"$ref":"#/components/schemas/Base64"},"proofValue":{"$ref":"#/components/schemas/Base64"},"proofPurpose":{"$ref":"#/components/schemas/ProofPurpose"},"verificationMethod":{"$ref":"#/components/schemas/DIDUri"},"created":{"type":"string"},"type":{"$ref":"#/components/schemas/ProofType"}},"required":["proofPurpose","verificationMethod","created","type"],"type":"object"},"Base64":{"type":"string"},"ProofPurpose":{"type":"string","enum":["assertionMethod","authentication","keyAgreement","contractAgreement","capabilityInvocation","capabilityDelegation"]},"DIDUri":{"type":"string"},"ProofType":{"type":"string","enum":["EcdsaSecp256k1Signature2019","EcdsaSecp256r1Signature2019","SchnorrSecp256k1Signature2019","Ed25519Signature2018","Ed25519Signature2020","JwtProof2020","JsonWebSignature2020"],"nullable":false},"Issuer":{"properties":{"name":{"type":"string"},"id":{"$ref":"#/components/schemas/DIDUri"}},"required":["id"],"type":"object","description":"Normalized representation of an issuer after processing InputIssuer."}}}}
```

Create a new credential offering with QR code\
\
Validates the credential subject against the schema and creates an offering\
that can be claimed by a holder. Generates a QR code for the offering.
----------------------------------------------------------------------

> Creates a new credential offering

```json
{"openapi":"3.0.0","info":{"title":"issuer-service","version":"1.0.0-rc2"},"servers":[{"url":"/"}],"security":[{"clientSecret":[]}],"components":{"securitySchemes":{"clientSecret":{"type":"apiKey","name":"x-client-secret","in":"header"}},"schemas":{"OfferingResponseDto":{"description":"Response DTO for offering details","properties":{"id":{"type":"string","description":"Unique identifier of the offering","format":"uuid"},"credential_issuer":{"type":"string","description":"Credential issuer URL","format":"uri"},"credential_configuration_ids":{"items":{"type":"string"},"type":"array","description":"List of credential configuration IDs"},"display":{"$ref":"#/components/schemas/OfferingDisplayDto","description":"Display information"},"grants":{"$ref":"#/components/schemas/OfferingGrantsDto","description":"Grant information"},"qr_code_url":{"type":"string","description":"QR code URL","format":"uri"},"offering_id":{"type":"string","description":"Offering ID","format":"uuid"},"offering_full_url":{"type":"string","description":"Full URL to the offering","format":"uri"},"redemption_limit":{"type":"number","format":"double","nullable":true,"description":"Redemption limit"},"expires_at":{"type":"string","format":"date-time","nullable":true,"description":"Expiration date"}},"required":["id","credential_issuer","credential_configuration_ids","display","grants"],"type":"object","additionalProperties":false},"OfferingDisplayDto":{"description":"Display information for the offering","properties":{"name":{"type":"string","description":"Name of the issuer"},"locale":{"type":"string","description":"Locale for the display"}},"required":["name","locale"],"type":"object","additionalProperties":false},"OfferingGrantsDto":{"description":"Grant information for the offering","properties":{"authorization_code":{"$ref":"#/components/schemas/AuthorizationCodeDto","description":"Authorization code grant type"}},"required":["authorization_code"],"type":"object","additionalProperties":false},"AuthorizationCodeDto":{"description":"Authorization code information","properties":{"issuer_state":{"type":"string","description":"Issuer state for the authorization code"}},"required":["issuer_state"],"type":"object","additionalProperties":false},"ErrorResponseDto":{"description":"Extended error response with HTTP details","properties":{"name":{"type":"string","description":"Error name"},"message":{"type":"string","description":"Error message"},"code":{"type":"number","format":"double","description":"Error code"},"statusCode":{"type":"number","format":"double","description":"HTTP status code"},"httpCode":{"type":"number","format":"double","description":"HTTP code"},"method":{"type":"string","description":"HTTP method"},"path":{"type":"string","description":"Request path"},"stack":{"type":"string","description":"Stack trace (tylko w środowisku deweloperskim)"}},"required":["name","message","code","statusCode","httpCode","method","path"],"type":"object","additionalProperties":false},"CreateOfferingRequestDto":{"description":"Request DTO for creating a new offering","properties":{"credential_type":{"type":"string","description":"Type of credential to be issued"},"credential_subject":{"$ref":"#/components/schemas/CredentialSubject","description":"Credential subject data"},"recipient":{"type":"string","description":"Optional recipient DID"},"redemption_limit":{"type":"number","format":"double","description":"Optional redemption limit"},"expires_at":{"type":"string","format":"date-time","description":"Optional expiration date"}},"required":["credential_type","credential_subject"],"type":"object","additionalProperties":false},"CredentialSubject":{"$ref":"#/components/schemas/Record_string.unknown_"},"Record_string.unknown_":{"properties":{},"additionalProperties":{},"type":"object","description":"Construct a type with a set of properties K of type T"}}},"paths":{"/api/v1/offerings":{"post":{"operationId":"CreateOffering","responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OfferingResponseDto"}}}},"404":{"description":"Not Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"description":"Creates a new credential offering","summary":"Create a new credential offering with QR code\n\nValidates the credential subject against the schema and creates an offering\nthat can be claimed by a holder. Generates a QR code for the offering.","tags":["Offering"],"parameters":[],"requestBody":{"description":"Request containing credential type, subject data and optional recipient","required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateOfferingRequestDto","description":"Request containing credential type, subject data and optional recipient"}}}}}}}}
```

## Get a specific offering by ID

> Retrieves an offering by its ID

```json
{"openapi":"3.0.0","info":{"title":"issuer-service","version":"1.0.0-rc2"},"servers":[{"url":"/"}],"security":[],"paths":{"/api/v1/offerings/{id}":{"get":{"operationId":"GetOffering","responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OfferingResponseDto"}}}},"404":{"description":"Not Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"description":"Retrieves an offering by its ID","summary":"Get a specific offering by ID","tags":["Offering"],"parameters":[{"description":"The unique identifier of the offering to retrieve","in":"path","name":"id","required":true,"schema":{"type":"string"}}]}}},"components":{"schemas":{"OfferingResponseDto":{"description":"Response DTO for offering details","properties":{"id":{"type":"string","description":"Unique identifier of the offering","format":"uuid"},"credential_issuer":{"type":"string","description":"Credential issuer URL","format":"uri"},"credential_configuration_ids":{"items":{"type":"string"},"type":"array","description":"List of credential configuration IDs"},"display":{"$ref":"#/components/schemas/OfferingDisplayDto","description":"Display information"},"grants":{"$ref":"#/components/schemas/OfferingGrantsDto","description":"Grant information"},"qr_code_url":{"type":"string","description":"QR code URL","format":"uri"},"offering_id":{"type":"string","description":"Offering ID","format":"uuid"},"offering_full_url":{"type":"string","description":"Full URL to the offering","format":"uri"},"redemption_limit":{"type":"number","format":"double","nullable":true,"description":"Redemption limit"},"expires_at":{"type":"string","format":"date-time","nullable":true,"description":"Expiration date"}},"required":["id","credential_issuer","credential_configuration_ids","display","grants"],"type":"object","additionalProperties":false},"OfferingDisplayDto":{"description":"Display information for the offering","properties":{"name":{"type":"string","description":"Name of the issuer"},"locale":{"type":"string","description":"Locale for the display"}},"required":["name","locale"],"type":"object","additionalProperties":false},"OfferingGrantsDto":{"description":"Grant information for the offering","properties":{"authorization_code":{"$ref":"#/components/schemas/AuthorizationCodeDto","description":"Authorization code grant type"}},"required":["authorization_code"],"type":"object","additionalProperties":false},"AuthorizationCodeDto":{"description":"Authorization code information","properties":{"issuer_state":{"type":"string","description":"Issuer state for the authorization code"}},"required":["issuer_state"],"type":"object","additionalProperties":false},"ErrorResponseDto":{"description":"Extended error response with HTTP details","properties":{"name":{"type":"string","description":"Error name"},"message":{"type":"string","description":"Error message"},"code":{"type":"number","format":"double","description":"Error code"},"statusCode":{"type":"number","format":"double","description":"HTTP status code"},"httpCode":{"type":"number","format":"double","description":"HTTP code"},"method":{"type":"string","description":"HTTP method"},"path":{"type":"string","description":"Request path"},"stack":{"type":"string","description":"Stack trace (tylko w środowisku deweloperskim)"}},"required":["name","message","code","statusCode","httpCode","method","path"],"type":"object","additionalProperties":false}}}}
```

## Get QR code for an offering

> Retrieves the QR code for a specific offering

```json
{"openapi":"3.0.0","info":{"title":"issuer-service","version":"1.0.0-rc2"},"servers":[{"url":"/"}],"security":[],"paths":{"/api/v1/offerings/{id}/qr-code":{"get":{"operationId":"GetOfferingQrCode","responses":{"200":{"description":"The QR code data for the offering","content":{"application/json":{"schema":{"type":"string"}}}},"404":{"description":"Not Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"description":"Retrieves the QR code for a specific offering","summary":"Get QR code for an offering","tags":["Offering"],"parameters":[{"description":"The unique identifier of the offering","in":"path","name":"id","required":true,"schema":{"type":"string"}}]}}},"components":{"schemas":{"ErrorResponseDto":{"description":"Extended error response with HTTP details","properties":{"name":{"type":"string","description":"Error name"},"message":{"type":"string","description":"Error message"},"code":{"type":"number","format":"double","description":"Error code"},"statusCode":{"type":"number","format":"double","description":"HTTP status code"},"httpCode":{"type":"number","format":"double","description":"HTTP code"},"method":{"type":"string","description":"HTTP method"},"path":{"type":"string","description":"Request path"},"stack":{"type":"string","description":"Stack trace (tylko w środowisku deweloperskim)"}},"required":["name","message","code","statusCode","httpCode","method","path"],"type":"object","additionalProperties":false}}}}
```

Issue a credential from an offering\
\
Creates a verifiable credential from an offering and assigns it to the authenticated holder
-------------------------------------------------------------------------------------------

> Issues a credential from an existing offering

```json
{"openapi":"3.0.0","info":{"title":"issuer-service","version":"1.0.0-rc2"},"servers":[{"url":"/"}],"security":[{"bearer":[]}],"components":{"securitySchemes":{"bearer":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"CredentialResponseDto":{"description":"Response DTO for credential operations","properties":{"id":{"type":"string","description":"Unique identifier of the credential","format":"uuid"},"vc":{"$ref":"#/components/schemas/VerifiableCredential","description":"Verifiable credential (if issued) in JWT format"}},"required":["id"],"type":"object","additionalProperties":false},"VerifiableCredential":{"$ref":"#/components/schemas/VC","description":"Represents a Verifiable Credential (VC) that follows the W3C Verifiable Credentials Data Model.\n\nA Verifiable Credential is a set of tamper-evident claims and metadata that cryptographically proves\nwho issued it, when it was issued, when it expires (if applicable), and provides a secure way\nto express claims about a subject (a person, organization, or thing).\n\nKey features:\n- Tamper-evident: Any changes to the credential after issuance can be detected\n- Cryptographically verified: Contains proofs that can verify the issuer and integrity\n- Privacy-respecting: Supports selective disclosure and zero-knowledge proofs\n- Machine-verifiable: Can be automatically verified without contacting the issuer\n\nThis implementation supports:\n- JSON serialization for storage and transmission\n- JWT serialization for compatibility with existing JWT infrastructures\n- Schema validation to ensure credential data conforms to defined structures\n- Cryptographic proof generation and verification"},"VC":{"$ref":"#/components/schemas/VCv1_1","description":"Current version of the Verifiable Credential format used in the implementation"},"VCv1_1":{"properties":{"evidence":{"additionalProperties":false,"type":"object","description":"Additional supporting information for the claims in the credential"},"credentialStatus":{"items":{"$ref":"#/components/schemas/CredentialStatus"},"type":"array","description":"Credential status information, such as revocation or suspension"},"credentialSchema":{"$ref":"#/components/schemas/CredentialSchema","description":"Schema that defines the structure of the credential subject"},"expirationDate":{"type":"string","description":"Date and time when the credential expires"},"proof":{"$ref":"#/components/schemas/Proof","description":"Cryptographic proof that can be used to detect tampering and verify the issuer"},"credentialSubject":{"anyOf":[{"$ref":"#/components/schemas/CredentialSubject"},{"items":{"$ref":"#/components/schemas/CredentialSubject"},"type":"array"}],"description":"Claims about the subject of the credential"},"issuanceDate":{"type":"string","description":"Date and time when the credential was issued"},"issuer":{"$ref":"#/components/schemas/Issuer","description":"Entity that issued the credential"},"id":{"$ref":"#/components/schemas/URI","description":"Unique identifier for the credential"},"type":{"items":{"type":"string"},"type":"array","description":"Credential type information that defines what the credential represents"},"@context":{"anyOf":[{"items":{"$ref":"#/components/schemas/URI"},"type":"array"},{"$ref":"#/components/schemas/URI"}],"description":"JSON-LD context that defines the vocabulary of the credential"}},"required":["credentialSubject","issuanceDate","issuer","type","@context"],"type":"object","description":"Verifiable Credential format following W3C VC Data Model 1.1 specification."},"CredentialStatus":{"description":"A single entry in a bitstring-based status list, per VC-DATA-MODEL-2.0","properties":{"id":{"$ref":"#/components/schemas/URI"},"type":{"type":"string","enum":["BitstringStatusListEntry"],"nullable":false},"statusPurpose":{"anyOf":[{"$ref":"#/components/schemas/StatusPurpose"},{"type":"string"}]},"statusListIndex":{"type":"number","format":"double"},"statusListCredential":{"$ref":"#/components/schemas/URI"},"statusSize":{"type":"number","format":"double"},"statusMessage":{"items":{"properties":{"message":{"type":"string"},"status":{"type":"string"}},"additionalProperties":{},"required":["message","status"],"type":"object"},"type":"array"}},"required":["type","statusPurpose","statusListIndex","statusListCredential"],"type":"object","additionalProperties":false},"URI":{"type":"string"},"StatusPurpose":{"enum":["refresh","revocation","suspension","message"],"type":"string"},"CredentialSchema":{"properties":{"type":{"type":"string","enum":["JsonSchemaValidator2018"],"nullable":false,"description":"The type of schema validator, currently only supporting JsonSchemaValidator2018"},"id":{"$ref":"#/components/schemas/URI","description":"The identifier for the schema, typically a URI pointing to the schema definition"}},"required":["type","id"],"type":"object","description":"Represents a credential schema reference that defines the structure of claims in a credential.\nFollowing W3C VC Data Model, this provides the means to validate credential contents."},"Proof":{"properties":{"challange":{"type":"string"},"nonce":{"type":"string"},"jws":{"$ref":"#/components/schemas/Base64"},"proofValue":{"$ref":"#/components/schemas/Base64"},"proofPurpose":{"$ref":"#/components/schemas/ProofPurpose"},"verificationMethod":{"$ref":"#/components/schemas/DIDUri"},"created":{"type":"string"},"type":{"$ref":"#/components/schemas/ProofType"}},"required":["proofPurpose","verificationMethod","created","type"],"type":"object"},"Base64":{"type":"string"},"ProofPurpose":{"type":"string","enum":["assertionMethod","authentication","keyAgreement","contractAgreement","capabilityInvocation","capabilityDelegation"]},"DIDUri":{"type":"string"},"ProofType":{"type":"string","enum":["EcdsaSecp256k1Signature2019","EcdsaSecp256r1Signature2019","SchnorrSecp256k1Signature2019","Ed25519Signature2018","Ed25519Signature2020","JwtProof2020","JsonWebSignature2020"],"nullable":false},"CredentialSubject":{"$ref":"#/components/schemas/Record_string.unknown_"},"Record_string.unknown_":{"properties":{},"additionalProperties":{},"type":"object","description":"Construct a type with a set of properties K of type T"},"Issuer":{"properties":{"name":{"type":"string"},"id":{"$ref":"#/components/schemas/DIDUri"}},"required":["id"],"type":"object","description":"Normalized representation of an issuer after processing InputIssuer."},"ErrorResponseDto":{"description":"Extended error response with HTTP details","properties":{"name":{"type":"string","description":"Error name"},"message":{"type":"string","description":"Error message"},"code":{"type":"number","format":"double","description":"Error code"},"statusCode":{"type":"number","format":"double","description":"HTTP status code"},"httpCode":{"type":"number","format":"double","description":"HTTP code"},"method":{"type":"string","description":"HTTP method"},"path":{"type":"string","description":"Request path"},"stack":{"type":"string","description":"Stack trace (tylko w środowisku deweloperskim)"}},"required":["name","message","code","statusCode","httpCode","method","path"],"type":"object","additionalProperties":false}}},"paths":{"/api/v1/offerings/{id}/claim":{"post":{"operationId":"ClaimCredentialFromOffering","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CredentialResponseDto"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Not Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"description":"Issues a credential from an existing offering","summary":"Issue a credential from an offering\n\nCreates a verifiable credential from an offering and assigns it to the authenticated holder","tags":["Offering"],"parameters":[{"description":"The unique identifier of the offering to issue","in":"path","name":"id","required":true,"schema":{"type":"string"}}]}}}}
```


# Metadata

## Retrieves the OpenID Credential Issuer metadata configuration

> Get OpenID Credential Issuer metadata

```json
{"openapi":"3.0.0","info":{"title":"issuer-service","version":"1.0.0-rc2"},"servers":[{"url":"/"}],"security":[],"paths":{"/.well-known/openid-credential-issuer":{"get":{"operationId":"IssuerMetadata","responses":{"200":{"description":"The issuer metadata","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IssuerMetadataDto"}}}}},"description":"Get OpenID Credential Issuer metadata","summary":"Retrieves the OpenID Credential Issuer metadata configuration","tags":["Metadata"],"parameters":[]}}},"components":{"schemas":{"IssuerMetadataDto":{"description":"OpenID Credential Issuer metadata","properties":{"credential_issuer":{"type":"string","description":"Base URL of the credential issuer"},"credential_endpoint":{"type":"string","description":"Endpoint for credential operations"},"token_endpoint":{"type":"string","description":"Endpoint for token operations"},"schema_endpoint":{"type":"string","description":"Endpoint for schema operations"},"authorize_endpoint":{"type":"string","description":"Endpoint for authorization requests"},"offering_endpoint":{"type":"string","description":"Endpoint for credential offerings"},"authorization_servers":{"items":{"type":"string"},"type":"array","description":"List of authorization server URLs"},"display":{"items":{"$ref":"#/components/schemas/DisplayDto"},"type":"array","description":"Display information for the issuer"},"credential_configurations_supported":{"$ref":"#/components/schemas/SupportedCredentialConfigurationDto","description":"Supported credential configurations"},"proof_types_supported":{"properties":{"jwt":{"properties":{"proof_signing_alg_values_supported":{"items":{"type":"string"},"type":"array"}},"required":["proof_signing_alg_values_supported"],"type":"object"}},"required":["jwt"],"type":"object","description":"Supported proof types and algorithms"}},"required":["credential_issuer","offering_endpoint","authorization_servers","display","credential_configurations_supported","proof_types_supported"],"type":"object","additionalProperties":false},"DisplayDto":{"description":"Display information for credential issuer","properties":{"name":{"type":"string","description":"Display name"},"locale":{"type":"string","description":"Locale code (BCP 47 language tag)"}},"required":["name","locale"],"type":"object","additionalProperties":false},"SupportedCredentialConfigurationDto":{"$ref":"#/components/schemas/Record_string.SupportedCredentialDefinitionDto_","description":"Mapping of credential names to their definitions"},"Record_string.SupportedCredentialDefinitionDto_":{"properties":{},"additionalProperties":{"$ref":"#/components/schemas/SupportedCredentialDefinitionDto"},"type":"object","description":"Construct a type with a set of properties K of type T"},"SupportedCredentialDefinitionDto":{"description":"Supported credential definition format and display","properties":{"format":{"type":"string","enum":["jwt_vc_json"],"nullable":false,"description":"Format of the credential"},"display":{"items":{"$ref":"#/components/schemas/DisplayDto"},"type":"array","description":"Display information for the credential"}},"required":["format","display"],"type":"object","additionalProperties":false}}}}
```

## Retrieves the DID metadata with domain linkage credential

> Get DID metadata

```json
{"openapi":"3.0.0","info":{"title":"issuer-service","version":"1.0.0-rc2"},"servers":[{"url":"/"}],"security":[],"paths":{"/.well-known/did-configuration":{"get":{"operationId":"DidMetadata","responses":{"200":{"description":"The DID metadata with JWT","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DidMetadataDto"}}}}},"description":"Get DID metadata","summary":"Retrieves the DID metadata with domain linkage credential","tags":["Metadata"],"parameters":[]}}},"components":{"schemas":{"DidMetadataDto":{"description":"DID metadata response","properties":{"entries":{"items":{"$ref":"#/components/schemas/EntryDto"},"type":"array","description":"List of DID metadata entries"}},"required":["entries"],"type":"object","additionalProperties":false},"EntryDto":{"description":"Entry for DID metadata","properties":{"did":{"type":"string","description":"DID of the issuer"},"jwt":{"type":"string","description":"JWT containing the domain linkage credential"}},"required":["did","jwt"],"type":"object","additionalProperties":false}}}}
```


# Credential

Create a credential without a recipient\
\
Creates a standalone credential without specifying a recipient
--------------------------------------------------------------

> Create a standalone credential

```json
{"openapi":"3.0.0","info":{"title":"issuer-service","version":"1.0.0-rc2"},"servers":[{"url":"/"}],"security":[{"bearer":[]}],"components":{"securitySchemes":{"bearer":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"CredentialResponseDto":{"description":"Response DTO for credential operations","properties":{"id":{"type":"string","description":"Unique identifier of the credential","format":"uuid"},"vc":{"$ref":"#/components/schemas/VerifiableCredential","description":"Verifiable credential (if issued) in JWT format"}},"required":["id"],"type":"object","additionalProperties":false},"VerifiableCredential":{"$ref":"#/components/schemas/VC","description":"Represents a Verifiable Credential (VC) that follows the W3C Verifiable Credentials Data Model.\n\nA Verifiable Credential is a set of tamper-evident claims and metadata that cryptographically proves\nwho issued it, when it was issued, when it expires (if applicable), and provides a secure way\nto express claims about a subject (a person, organization, or thing).\n\nKey features:\n- Tamper-evident: Any changes to the credential after issuance can be detected\n- Cryptographically verified: Contains proofs that can verify the issuer and integrity\n- Privacy-respecting: Supports selective disclosure and zero-knowledge proofs\n- Machine-verifiable: Can be automatically verified without contacting the issuer\n\nThis implementation supports:\n- JSON serialization for storage and transmission\n- JWT serialization for compatibility with existing JWT infrastructures\n- Schema validation to ensure credential data conforms to defined structures\n- Cryptographic proof generation and verification"},"VC":{"$ref":"#/components/schemas/VCv1_1","description":"Current version of the Verifiable Credential format used in the implementation"},"VCv1_1":{"properties":{"evidence":{"additionalProperties":false,"type":"object","description":"Additional supporting information for the claims in the credential"},"credentialStatus":{"items":{"$ref":"#/components/schemas/CredentialStatus"},"type":"array","description":"Credential status information, such as revocation or suspension"},"credentialSchema":{"$ref":"#/components/schemas/CredentialSchema","description":"Schema that defines the structure of the credential subject"},"expirationDate":{"type":"string","description":"Date and time when the credential expires"},"proof":{"$ref":"#/components/schemas/Proof","description":"Cryptographic proof that can be used to detect tampering and verify the issuer"},"credentialSubject":{"anyOf":[{"$ref":"#/components/schemas/CredentialSubject"},{"items":{"$ref":"#/components/schemas/CredentialSubject"},"type":"array"}],"description":"Claims about the subject of the credential"},"issuanceDate":{"type":"string","description":"Date and time when the credential was issued"},"issuer":{"$ref":"#/components/schemas/Issuer","description":"Entity that issued the credential"},"id":{"$ref":"#/components/schemas/URI","description":"Unique identifier for the credential"},"type":{"items":{"type":"string"},"type":"array","description":"Credential type information that defines what the credential represents"},"@context":{"anyOf":[{"items":{"$ref":"#/components/schemas/URI"},"type":"array"},{"$ref":"#/components/schemas/URI"}],"description":"JSON-LD context that defines the vocabulary of the credential"}},"required":["credentialSubject","issuanceDate","issuer","type","@context"],"type":"object","description":"Verifiable Credential format following W3C VC Data Model 1.1 specification."},"CredentialStatus":{"description":"A single entry in a bitstring-based status list, per VC-DATA-MODEL-2.0","properties":{"id":{"$ref":"#/components/schemas/URI"},"type":{"type":"string","enum":["BitstringStatusListEntry"],"nullable":false},"statusPurpose":{"anyOf":[{"$ref":"#/components/schemas/StatusPurpose"},{"type":"string"}]},"statusListIndex":{"type":"number","format":"double"},"statusListCredential":{"$ref":"#/components/schemas/URI"},"statusSize":{"type":"number","format":"double"},"statusMessage":{"items":{"properties":{"message":{"type":"string"},"status":{"type":"string"}},"additionalProperties":{},"required":["message","status"],"type":"object"},"type":"array"}},"required":["type","statusPurpose","statusListIndex","statusListCredential"],"type":"object","additionalProperties":false},"URI":{"type":"string"},"StatusPurpose":{"enum":["refresh","revocation","suspension","message"],"type":"string"},"CredentialSchema":{"properties":{"type":{"type":"string","enum":["JsonSchemaValidator2018"],"nullable":false,"description":"The type of schema validator, currently only supporting JsonSchemaValidator2018"},"id":{"$ref":"#/components/schemas/URI","description":"The identifier for the schema, typically a URI pointing to the schema definition"}},"required":["type","id"],"type":"object","description":"Represents a credential schema reference that defines the structure of claims in a credential.\nFollowing W3C VC Data Model, this provides the means to validate credential contents."},"Proof":{"properties":{"challange":{"type":"string"},"nonce":{"type":"string"},"jws":{"$ref":"#/components/schemas/Base64"},"proofValue":{"$ref":"#/components/schemas/Base64"},"proofPurpose":{"$ref":"#/components/schemas/ProofPurpose"},"verificationMethod":{"$ref":"#/components/schemas/DIDUri"},"created":{"type":"string"},"type":{"$ref":"#/components/schemas/ProofType"}},"required":["proofPurpose","verificationMethod","created","type"],"type":"object"},"Base64":{"type":"string"},"ProofPurpose":{"type":"string","enum":["assertionMethod","authentication","keyAgreement","contractAgreement","capabilityInvocation","capabilityDelegation"]},"DIDUri":{"type":"string"},"ProofType":{"type":"string","enum":["EcdsaSecp256k1Signature2019","EcdsaSecp256r1Signature2019","SchnorrSecp256k1Signature2019","Ed25519Signature2018","Ed25519Signature2020","JwtProof2020","JsonWebSignature2020"],"nullable":false},"CredentialSubject":{"$ref":"#/components/schemas/Record_string.unknown_"},"Record_string.unknown_":{"properties":{},"additionalProperties":{},"type":"object","description":"Construct a type with a set of properties K of type T"},"Issuer":{"properties":{"name":{"type":"string"},"id":{"$ref":"#/components/schemas/DIDUri"}},"required":["id"],"type":"object","description":"Normalized representation of an issuer after processing InputIssuer."},"ErrorResponseDto":{"description":"Extended error response with HTTP details","properties":{"name":{"type":"string","description":"Error name"},"message":{"type":"string","description":"Error message"},"code":{"type":"number","format":"double","description":"Error code"},"statusCode":{"type":"number","format":"double","description":"HTTP status code"},"httpCode":{"type":"number","format":"double","description":"HTTP code"},"method":{"type":"string","description":"HTTP method"},"path":{"type":"string","description":"Request path"},"stack":{"type":"string","description":"Stack trace (tylko w środowisku deweloperskim)"}},"required":["name","message","code","statusCode","httpCode","method","path"],"type":"object","additionalProperties":false},"CreateCredentialDto":{"description":"Request DTO for creating a standalone credential","properties":{"type":{"type":"string","description":"Type of credential to be created","minLength":1,"pattern":"^[a-zA-Z0-9_]+$"},"credential_subject":{"$ref":"#/components/schemas/CredentialSubject","description":"Credential subject data - the claims to be included in the credential"}},"required":["type","credential_subject"],"type":"object","additionalProperties":false}}},"paths":{"/api/v1/credentials":{"post":{"operationId":"CreateStandaloneCredential","responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CredentialResponseDto"}}}},"400":{"description":"Bad Request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"description":"Create a standalone credential","summary":"Create a credential without a recipient\n\nCreates a standalone credential without specifying a recipient","tags":["Credential"],"parameters":[],"requestBody":{"description":"Request containing credential type and subject data","required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCredentialDto","description":"Request containing credential type and subject data"}}}}}}}}
```

Get a credential by its ID\
\
Gets a credential by its ID for the authenticated holder
--------------------------------------------------------

> Retrieve a credential by ID

```json
{"openapi":"3.0.0","info":{"title":"issuer-service","version":"1.0.0-rc2"},"servers":[{"url":"/"}],"security":[{"bearer":[]}],"components":{"securitySchemes":{"bearer":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"CredentialResponseDto":{"description":"Response DTO for credential operations","properties":{"id":{"type":"string","description":"Unique identifier of the credential","format":"uuid"},"vc":{"$ref":"#/components/schemas/VerifiableCredential","description":"Verifiable credential (if issued) in JWT format"}},"required":["id"],"type":"object","additionalProperties":false},"VerifiableCredential":{"$ref":"#/components/schemas/VC","description":"Represents a Verifiable Credential (VC) that follows the W3C Verifiable Credentials Data Model.\n\nA Verifiable Credential is a set of tamper-evident claims and metadata that cryptographically proves\nwho issued it, when it was issued, when it expires (if applicable), and provides a secure way\nto express claims about a subject (a person, organization, or thing).\n\nKey features:\n- Tamper-evident: Any changes to the credential after issuance can be detected\n- Cryptographically verified: Contains proofs that can verify the issuer and integrity\n- Privacy-respecting: Supports selective disclosure and zero-knowledge proofs\n- Machine-verifiable: Can be automatically verified without contacting the issuer\n\nThis implementation supports:\n- JSON serialization for storage and transmission\n- JWT serialization for compatibility with existing JWT infrastructures\n- Schema validation to ensure credential data conforms to defined structures\n- Cryptographic proof generation and verification"},"VC":{"$ref":"#/components/schemas/VCv1_1","description":"Current version of the Verifiable Credential format used in the implementation"},"VCv1_1":{"properties":{"evidence":{"additionalProperties":false,"type":"object","description":"Additional supporting information for the claims in the credential"},"credentialStatus":{"items":{"$ref":"#/components/schemas/CredentialStatus"},"type":"array","description":"Credential status information, such as revocation or suspension"},"credentialSchema":{"$ref":"#/components/schemas/CredentialSchema","description":"Schema that defines the structure of the credential subject"},"expirationDate":{"type":"string","description":"Date and time when the credential expires"},"proof":{"$ref":"#/components/schemas/Proof","description":"Cryptographic proof that can be used to detect tampering and verify the issuer"},"credentialSubject":{"anyOf":[{"$ref":"#/components/schemas/CredentialSubject"},{"items":{"$ref":"#/components/schemas/CredentialSubject"},"type":"array"}],"description":"Claims about the subject of the credential"},"issuanceDate":{"type":"string","description":"Date and time when the credential was issued"},"issuer":{"$ref":"#/components/schemas/Issuer","description":"Entity that issued the credential"},"id":{"$ref":"#/components/schemas/URI","description":"Unique identifier for the credential"},"type":{"items":{"type":"string"},"type":"array","description":"Credential type information that defines what the credential represents"},"@context":{"anyOf":[{"items":{"$ref":"#/components/schemas/URI"},"type":"array"},{"$ref":"#/components/schemas/URI"}],"description":"JSON-LD context that defines the vocabulary of the credential"}},"required":["credentialSubject","issuanceDate","issuer","type","@context"],"type":"object","description":"Verifiable Credential format following W3C VC Data Model 1.1 specification."},"CredentialStatus":{"description":"A single entry in a bitstring-based status list, per VC-DATA-MODEL-2.0","properties":{"id":{"$ref":"#/components/schemas/URI"},"type":{"type":"string","enum":["BitstringStatusListEntry"],"nullable":false},"statusPurpose":{"anyOf":[{"$ref":"#/components/schemas/StatusPurpose"},{"type":"string"}]},"statusListIndex":{"type":"number","format":"double"},"statusListCredential":{"$ref":"#/components/schemas/URI"},"statusSize":{"type":"number","format":"double"},"statusMessage":{"items":{"properties":{"message":{"type":"string"},"status":{"type":"string"}},"additionalProperties":{},"required":["message","status"],"type":"object"},"type":"array"}},"required":["type","statusPurpose","statusListIndex","statusListCredential"],"type":"object","additionalProperties":false},"URI":{"type":"string"},"StatusPurpose":{"enum":["refresh","revocation","suspension","message"],"type":"string"},"CredentialSchema":{"properties":{"type":{"type":"string","enum":["JsonSchemaValidator2018"],"nullable":false,"description":"The type of schema validator, currently only supporting JsonSchemaValidator2018"},"id":{"$ref":"#/components/schemas/URI","description":"The identifier for the schema, typically a URI pointing to the schema definition"}},"required":["type","id"],"type":"object","description":"Represents a credential schema reference that defines the structure of claims in a credential.\nFollowing W3C VC Data Model, this provides the means to validate credential contents."},"Proof":{"properties":{"challange":{"type":"string"},"nonce":{"type":"string"},"jws":{"$ref":"#/components/schemas/Base64"},"proofValue":{"$ref":"#/components/schemas/Base64"},"proofPurpose":{"$ref":"#/components/schemas/ProofPurpose"},"verificationMethod":{"$ref":"#/components/schemas/DIDUri"},"created":{"type":"string"},"type":{"$ref":"#/components/schemas/ProofType"}},"required":["proofPurpose","verificationMethod","created","type"],"type":"object"},"Base64":{"type":"string"},"ProofPurpose":{"type":"string","enum":["assertionMethod","authentication","keyAgreement","contractAgreement","capabilityInvocation","capabilityDelegation"]},"DIDUri":{"type":"string"},"ProofType":{"type":"string","enum":["EcdsaSecp256k1Signature2019","EcdsaSecp256r1Signature2019","SchnorrSecp256k1Signature2019","Ed25519Signature2018","Ed25519Signature2020","JwtProof2020","JsonWebSignature2020"],"nullable":false},"CredentialSubject":{"$ref":"#/components/schemas/Record_string.unknown_"},"Record_string.unknown_":{"properties":{},"additionalProperties":{},"type":"object","description":"Construct a type with a set of properties K of type T"},"Issuer":{"properties":{"name":{"type":"string"},"id":{"$ref":"#/components/schemas/DIDUri"}},"required":["id"],"type":"object","description":"Normalized representation of an issuer after processing InputIssuer."},"ErrorResponseDto":{"description":"Extended error response with HTTP details","properties":{"name":{"type":"string","description":"Error name"},"message":{"type":"string","description":"Error message"},"code":{"type":"number","format":"double","description":"Error code"},"statusCode":{"type":"number","format":"double","description":"HTTP status code"},"httpCode":{"type":"number","format":"double","description":"HTTP code"},"method":{"type":"string","description":"HTTP method"},"path":{"type":"string","description":"Request path"},"stack":{"type":"string","description":"Stack trace (tylko w środowisku deweloperskim)"}},"required":["name","message","code","statusCode","httpCode","method","path"],"type":"object","additionalProperties":false}}},"paths":{"/api/v1/credentials/{id}":{"get":{"operationId":"HandleRetrieveCredentialById","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CredentialResponseDto"}}}},"400":{"description":"Bad Request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Not Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"description":"Retrieve a credential by ID","summary":"Get a credential by its ID\n\nGets a credential by its ID for the authenticated holder","tags":["Credential"],"parameters":[{"description":"The unique identifier of the credential","in":"path","name":"id","required":true,"schema":{"type":"string"}}]}}}}
```

## Update a single credential’s status bit

> Mark a credential’s status (e.g. revoke)

```json
{"openapi":"3.0.0","info":{"title":"issuer-service","version":"1.0.0-rc2"},"servers":[{"url":"/"}],"security":[{"clientSecret":[]}],"components":{"securitySchemes":{"clientSecret":{"type":"apiKey","name":"x-client-secret","in":"header"}},"schemas":{"ErrorResponseDto":{"description":"Extended error response with HTTP details","properties":{"name":{"type":"string","description":"Error name"},"message":{"type":"string","description":"Error message"},"code":{"type":"number","format":"double","description":"Error code"},"statusCode":{"type":"number","format":"double","description":"HTTP status code"},"httpCode":{"type":"number","format":"double","description":"HTTP code"},"method":{"type":"string","description":"HTTP method"},"path":{"type":"string","description":"Request path"},"stack":{"type":"string","description":"Stack trace (tylko w środowisku deweloperskim)"}},"required":["name","message","code","statusCode","httpCode","method","path"],"type":"object","additionalProperties":false}}},"paths":{"/api/v1/credentials/{id}/revoke":{"post":{"operationId":"RevokeCredential","responses":{"200":{"description":"OK"},"400":{"description":"Bad Request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"description":"Mark a credential’s status (e.g. revoke)","summary":"Update a single credential’s status bit","tags":["Credential"],"parameters":[{"description":"The credential ID to update","in":"path","name":"id","required":true,"schema":{"type":"string"}}]}}}}
```


# Blockchain

## Get the issuer's blockchain address

> Retrieves the blockchain address used by the issuer

```json
{"openapi":"3.0.0","info":{"title":"issuer-service","version":"1.0.0-rc2"},"servers":[{"url":"/"}],"security":[],"paths":{"/api/v1/blockchain/address":{"get":{"operationId":"GetBlockchainAddressHandler","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BlockchainAddressResponseDto"}}}},"404":{"description":"Not Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"description":"Retrieves the blockchain address used by the issuer","summary":"Get the issuer's blockchain address","tags":["Blockchain"],"parameters":[]}}},"components":{"schemas":{"BlockchainAddressResponseDto":{"description":"Response DTO for blockchain address","properties":{"address":{"type":"string","description":"Blockchain address of the issuer","minLength":1,"pattern":"^0x[a-fA-F0-9]{40}$"}},"required":["address"],"type":"object","additionalProperties":false},"ErrorResponseDto":{"description":"Extended error response with HTTP details","properties":{"name":{"type":"string","description":"Error name"},"message":{"type":"string","description":"Error message"},"code":{"type":"number","format":"double","description":"Error code"},"statusCode":{"type":"number","format":"double","description":"HTTP status code"},"httpCode":{"type":"number","format":"double","description":"HTTP code"},"method":{"type":"string","description":"HTTP method"},"path":{"type":"string","description":"Request path"},"stack":{"type":"string","description":"Stack trace (tylko w środowisku deweloperskim)"}},"required":["name","message","code","statusCode","httpCode","method","path"],"type":"object","additionalProperties":false}}}}
```

Anchor the issuer's DID on blockchain\
\
Creates a blockchain transaction to register the issuer's DID
-------------------------------------------------------------

> Anchors the issuer's DID on the blockchain

```json
{"openapi":"3.0.0","info":{"title":"issuer-service","version":"1.0.0-rc2"},"servers":[{"url":"/"}],"security":[],"paths":{"/api/v1/blockchain/dids":{"post":{"operationId":"AnchorDIDOnBlockchainHandler","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnchorDIDResponseDto"}}}},"500":{"description":"Server Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"description":"Anchors the issuer's DID on the blockchain","summary":"Anchor the issuer's DID on blockchain\n\nCreates a blockchain transaction to register the issuer's DID","tags":["Blockchain"],"parameters":[]}}},"components":{"schemas":{"AnchorDIDResponseDto":{"description":"Response DTO for anchoring DID on blockchain","properties":{"did":{"type":"string","description":"DID anchored on blockchain"}},"required":["did"],"type":"object","additionalProperties":false},"ErrorResponseDto":{"description":"Extended error response with HTTP details","properties":{"name":{"type":"string","description":"Error name"},"message":{"type":"string","description":"Error message"},"code":{"type":"number","format":"double","description":"Error code"},"statusCode":{"type":"number","format":"double","description":"HTTP status code"},"httpCode":{"type":"number","format":"double","description":"HTTP code"},"method":{"type":"string","description":"HTTP method"},"path":{"type":"string","description":"Request path"},"stack":{"type":"string","description":"Stack trace (tylko w środowisku deweloperskim)"}},"required":["name","message","code","statusCode","httpCode","method","path"],"type":"object","additionalProperties":false}}}}
```

Verify a DID on the blockchain\
\
Retrieves and validates a DID document from the blockchain
----------------------------------------------------------

> Verifies if a DID exists on the blockchain

```json
{"openapi":"3.0.0","info":{"title":"issuer-service","version":"1.0.0-rc2"},"servers":[{"url":"/"}],"security":[],"paths":{"/api/v1/blockchain/dids/{did}":{"get":{"operationId":"VerifyDIDOnBlockchainHandler","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyDIDResponseDto"}}}},"404":{"description":"Not Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"500":{"description":"Server Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"description":"Verifies if a DID exists on the blockchain","summary":"Verify a DID on the blockchain\n\nRetrieves and validates a DID document from the blockchain","tags":["Blockchain"],"parameters":[{"description":"The DID to verify","in":"path","name":"did","required":true,"schema":{"type":"string"}}]}}},"components":{"schemas":{"VerifyDIDResponseDto":{"description":"Response DTO for verifying DID on blockchain","properties":{"did":{"$ref":"#/components/schemas/DidDocument","description":"DID Document from blockchain"}},"required":["did"],"type":"object","additionalProperties":false},"DidDocument":{"properties":{"id":{"type":"string"},"context":{"items":{"type":"string"},"type":"array"},"controller":{"items":{"type":"string"},"type":"array"},"verificationMethod":{"items":{"$ref":"#/components/schemas/DidVerificationMethod"},"type":"array"},"authentication":{"items":{"$ref":"#/components/schemas/DidVerificationRelationship"},"type":"array"},"assertionMethod":{"items":{"$ref":"#/components/schemas/DidVerificationRelationship"},"type":"array"},"keyAgreement":{"items":{"$ref":"#/components/schemas/DidVerificationRelationship"},"type":"array"},"capabilityInvocation":{"items":{"$ref":"#/components/schemas/DidVerificationRelationship"},"type":"array"},"capabilityDelegation":{"items":{"$ref":"#/components/schemas/DidVerificationRelationship"},"type":"array"},"service":{"items":{"$ref":"#/components/schemas/DidDocumentService"},"type":"array"},"alsoKnownAs":{"items":{"type":"string"},"type":"array"}},"required":["id","context","controller","verificationMethod","authentication","assertionMethod","keyAgreement","capabilityInvocation","capabilityDelegation","service","alsoKnownAs"],"type":"object","additionalProperties":false},"DidVerificationMethod":{"properties":{"id":{"type":"string"},"type":{"type":"string"},"controller":{"type":"string"},"publicKeyBase58":{"type":"string"},"publicKeyMultibase":{"type":"string"},"publicKeyJwk":{"$ref":"#/components/schemas/JsonWebKey"}},"required":["id","type","controller"],"type":"object","additionalProperties":false},"JsonWebKey":{"properties":{"kty":{"type":"string"},"crv":{"type":"string"},"x":{"type":"string"},"y":{"type":"string"}},"required":["kty","crv","x","y"],"type":"object","additionalProperties":false},"DidVerificationRelationship":{"properties":{"referenceId":{"type":"string"},"embeddedMethod":{"$ref":"#/components/schemas/DidVerificationMethod"}},"type":"object","additionalProperties":false},"DidDocumentService":{"properties":{"id":{"type":"string"},"type":{"type":"string"},"serviceEndpoint":{"items":{"type":"string"},"type":"array"}},"required":["id","type","serviceEndpoint"],"type":"object","additionalProperties":false},"ErrorResponseDto":{"description":"Extended error response with HTTP details","properties":{"name":{"type":"string","description":"Error name"},"message":{"type":"string","description":"Error message"},"code":{"type":"number","format":"double","description":"Error code"},"statusCode":{"type":"number","format":"double","description":"HTTP status code"},"httpCode":{"type":"number","format":"double","description":"HTTP code"},"method":{"type":"string","description":"HTTP method"},"path":{"type":"string","description":"Request path"},"stack":{"type":"string","description":"Stack trace (tylko w środowisku deweloperskim)"}},"required":["name","message","code","statusCode","httpCode","method","path"],"type":"object","additionalProperties":false}}}}
```

## Update the revocation list on the blockchain

> Updates the revocation list on the blockchain based on the current status of the credentials\
> It fetches all credentials from the database, checks their status, and updates the revocation list accordingly.

```json
{"openapi":"3.0.0","info":{"title":"issuer-service","version":"1.0.0-rc2"},"servers":[{"url":"/"}],"security":[{"clientSecret":[]}],"components":{"securitySchemes":{"clientSecret":{"type":"apiKey","name":"x-client-secret","in":"header"}},"schemas":{"ErrorResponseDto":{"description":"Extended error response with HTTP details","properties":{"name":{"type":"string","description":"Error name"},"message":{"type":"string","description":"Error message"},"code":{"type":"number","format":"double","description":"Error code"},"statusCode":{"type":"number","format":"double","description":"HTTP status code"},"httpCode":{"type":"number","format":"double","description":"HTTP code"},"method":{"type":"string","description":"HTTP method"},"path":{"type":"string","description":"Request path"},"stack":{"type":"string","description":"Stack trace (tylko w środowisku deweloperskim)"}},"required":["name","message","code","statusCode","httpCode","method","path"],"type":"object","additionalProperties":false}}},"paths":{"/api/v1/blockchain/revocations/{id}":{"post":{"operationId":"UpdateRevocationList","responses":{"200":{"description":"OK"},"400":{"description":"Bad Request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"description":"Updates the revocation list on the blockchain based on the current status of the credentials\nIt fetches all credentials from the database, checks their status, and updates the revocation list accordingly.","summary":"Update the revocation list on the blockchain","tags":["Blockchain"],"parameters":[{"description":"The ID of the revocation list to update","in":"path","name":"id","required":true,"schema":{"format":"double","type":"number"}}]}}}}
```


# Authorization

Create a new authorization request\
\
This endpoint creates a new authorization request record that will be used\
later in the token exchange process
-----------------------------------

> Creates a new authorization request

```json
{"openapi":"3.0.0","info":{"title":"issuer-service","version":"1.0.0-rc2"},"servers":[{"url":"/"}],"security":[],"paths":{"/api/v1/authorizations":{"post":{"operationId":"CreateAuthorizationRequest","responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthorizationRequestResponseDto"}}}},"400":{"description":"Bad Request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"description":"Creates a new authorization request","summary":"Create a new authorization request\n\nThis endpoint creates a new authorization request record that will be used\nlater in the token exchange process","tags":["Authorization"],"parameters":[],"requestBody":{"description":"Request with presentation definition and security parameters","required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAuthorizationRequestDto","description":"Request with presentation definition and security parameters"}}}}}}},"components":{"schemas":{"AuthorizationRequestResponseDto":{"description":"Response DTO for a new authorization request","properties":{"id":{"type":"string","description":"The unique identifier of the authorization request","format":"uuid"},"presentation_definition":{"$ref":"#/components/schemas/PresentationDefinition","description":"The presentation definition that defines what credentials are required"},"state":{"type":"string","description":"Random string to prevent CSRF attacks","minLength":32,"maxLength":64},"nonce":{"type":"string","description":"Random string to ensure request uniqueness","minLength":32,"maxLength":64},"expires_at":{"type":"string","format":"date-time","description":"When the authorization request expires"},"code":{"type":"string","nullable":true,"description":"Code for authorization (may be null)"},"holder_did":{"type":"string","nullable":true,"description":"Holder DID (may be null)","pattern":"^did:[a-zA-Z0-9]+:[a-zA-Z0-9:.%-]+$"},"used":{"type":"boolean","description":"Whether the authorization request has been used"}},"required":["id","presentation_definition","state","nonce","expires_at"],"type":"object","additionalProperties":false},"PresentationDefinition":{"properties":{"input_descriptors":{"items":{"$ref":"#/components/schemas/InputDescriptor"},"type":"array","description":"List of input descriptors defining required credentials"},"id":{"type":"string","description":"Unique identifier for this presentation definition"}},"required":["input_descriptors","id"],"type":"object","description":"Defines requirements for a verifiable presentation.\nUsed by verifiers to specify what credentials they require."},"InputDescriptor":{"properties":{"constraints":{"properties":{"fields":{"items":{"$ref":"#/components/schemas/InputDescriptorField"},"type":"array","description":"Fields that must be present in the credential"},"limit_disclosure":{"type":"string","enum":["required"],"nullable":false,"description":"Defines disclosure requirements for credential fields"}},"required":["fields","limit_disclosure"],"type":"object","description":"Constraints that credentials must satisfy"},"id":{"type":"string","description":"Unique identifier for this input descriptor"}},"required":["constraints","id"],"type":"object","description":"Describes requirements for a credential in a presentation.\nUsed to define constraints on what credentials are acceptable\nfor a given verification purpose."},"InputDescriptorField":{"properties":{"filter":{"$ref":"#/components/schemas/JSONSchema7","description":"Optional JSON Schema that values must conform to"},"path":{"items":{"type":"string"},"type":"array","description":"Array of JSONPath expressions to locate values in the credential"}},"required":["path"],"type":"object","description":"Describes a field within an input descriptor.\nUsed to specify which paths to extract from a credential\nand optional JSON Schema filter to validate the values."},"JSONSchema7":{"properties":{"$id":{"type":"string"},"$ref":{"type":"string"},"$schema":{"$ref":"#/components/schemas/JSONSchema7Version"},"$comment":{"type":"string"},"$defs":{"properties":{},"additionalProperties":{"$ref":"#/components/schemas/JSONSchema7Definition"},"type":"object"},"type":{"anyOf":[{"$ref":"#/components/schemas/JSONSchema7TypeName"},{"items":{"$ref":"#/components/schemas/JSONSchema7TypeName"},"type":"array"}]},"enum":{"items":{"$ref":"#/components/schemas/JSONSchema7Type"},"type":"array"},"const":{"$ref":"#/components/schemas/JSONSchema7Type"},"multipleOf":{"type":"number","format":"double"},"maximum":{"type":"number","format":"double"},"exclusiveMaximum":{"type":"number","format":"double"},"minimum":{"type":"number","format":"double"},"exclusiveMinimum":{"type":"number","format":"double"},"maxLength":{"type":"number","format":"double"},"minLength":{"type":"number","format":"double"},"pattern":{"type":"string"},"items":{"anyOf":[{"$ref":"#/components/schemas/JSONSchema7Definition"},{"items":{"$ref":"#/components/schemas/JSONSchema7Definition"},"type":"array"}]},"additionalItems":{"$ref":"#/components/schemas/JSONSchema7Definition"},"maxItems":{"type":"number","format":"double"},"minItems":{"type":"number","format":"double"},"uniqueItems":{"type":"boolean"},"contains":{"$ref":"#/components/schemas/JSONSchema7Definition"},"maxProperties":{"type":"number","format":"double"},"minProperties":{"type":"number","format":"double"},"required":{"items":{"type":"string"},"type":"array"},"properties":{"properties":{},"additionalProperties":{"$ref":"#/components/schemas/JSONSchema7Definition"},"type":"object"},"patternProperties":{"properties":{},"additionalProperties":{"$ref":"#/components/schemas/JSONSchema7Definition"},"type":"object"},"additionalProperties":{"$ref":"#/components/schemas/JSONSchema7Definition"},"dependencies":{"properties":{},"additionalProperties":{"anyOf":[{"$ref":"#/components/schemas/JSONSchema7Definition"},{"items":{"type":"string"},"type":"array"}]},"type":"object"},"propertyNames":{"$ref":"#/components/schemas/JSONSchema7Definition"},"if":{"$ref":"#/components/schemas/JSONSchema7Definition"},"then":{"$ref":"#/components/schemas/JSONSchema7Definition"},"else":{"$ref":"#/components/schemas/JSONSchema7Definition"},"allOf":{"items":{"$ref":"#/components/schemas/JSONSchema7Definition"},"type":"array"},"anyOf":{"items":{"$ref":"#/components/schemas/JSONSchema7Definition"},"type":"array"},"oneOf":{"items":{"$ref":"#/components/schemas/JSONSchema7Definition"},"type":"array"},"not":{"$ref":"#/components/schemas/JSONSchema7Definition"},"format":{"type":"string"},"contentMediaType":{"type":"string"},"contentEncoding":{"type":"string"},"definitions":{"properties":{},"additionalProperties":{"$ref":"#/components/schemas/JSONSchema7Definition"},"type":"object"},"title":{"type":"string"},"description":{"type":"string"},"default":{"$ref":"#/components/schemas/JSONSchema7Type"},"readOnly":{"type":"boolean"},"writeOnly":{"type":"boolean"}},"type":"object","additionalProperties":false},"JSONSchema7Version":{"type":"string","description":"Meta schema\n\nRecommended values:\n- 'http://json-schema.org/schema#'\n- 'http://json-schema.org/hyper-schema#'\n- 'http://json-schema.org/draft-07/schema#'\n- 'http://json-schema.org/draft-07/hyper-schema#'"},"JSONSchema7Definition":{"anyOf":[{"$ref":"#/components/schemas/JSONSchema7"},{"type":"boolean"}],"description":"JSON Schema v7"},"JSONSchema7TypeName":{"type":"string","enum":["string","number","integer","boolean","object","array","null"],"description":"Primitive type"},"JSONSchema7Type":{"anyOf":[{"type":"string"},{"type":"number","format":"double"},{"type":"boolean"},{"$ref":"#/components/schemas/JSONSchema7Object"},{"$ref":"#/components/schemas/JSONSchema7Array"}],"nullable":true,"description":"Primitive type"},"JSONSchema7Object":{"properties":{},"type":"object","additionalProperties":{"$ref":"#/components/schemas/JSONSchema7Type"}},"JSONSchema7Array":{"properties":{},"type":"object","additionalProperties":false},"ErrorResponseDto":{"description":"Extended error response with HTTP details","properties":{"name":{"type":"string","description":"Error name"},"message":{"type":"string","description":"Error message"},"code":{"type":"number","format":"double","description":"Error code"},"statusCode":{"type":"number","format":"double","description":"HTTP status code"},"httpCode":{"type":"number","format":"double","description":"HTTP code"},"method":{"type":"string","description":"HTTP method"},"path":{"type":"string","description":"Request path"},"stack":{"type":"string","description":"Stack trace (tylko w środowisku deweloperskim)"}},"required":["name","message","code","statusCode","httpCode","method","path"],"type":"object","additionalProperties":false},"CreateAuthorizationRequestDto":{"description":"Request DTO for creating a new authorization request","properties":{"presentation_definition":{"$ref":"#/components/schemas/PresentationDefinition","description":"The presentation definition that defines what credentials are required"},"state":{"type":"string","description":"Random string to prevent CSRF attacks"},"nonce":{"type":"string","description":"Random string to ensure request uniqueness"}},"required":["presentation_definition","state","nonce"],"type":"object","additionalProperties":false}}}}
```

Validate an authorization request with VP\
\
This endpoint is called by the client (wallet) to validate the authorization request.\
It validates the required parameters and the Verifiable Presentation against\
the associated presentation definition.
---------------------------------------

> Validates an authorization request with a verifiable presentation

```json
{"openapi":"3.0.0","info":{"title":"issuer-service","version":"1.0.0-rc2"},"servers":[{"url":"/"}],"security":[],"paths":{"/api/v1/authorizations/{id}/validate":{"post":{"operationId":"ValidateAuthorizationRequest","responses":{"200":{"description":"Validated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidateAuthorizationResponseDto"}}}},"400":{"description":"Bad Request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Not Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"description":"Validates an authorization request with a verifiable presentation","summary":"Validate an authorization request with VP\n\nThis endpoint is called by the client (wallet) to validate the authorization request.\nIt validates the required parameters and the Verifiable Presentation against\nthe associated presentation definition.","tags":["Authorization"],"parameters":[{"description":"The unique identifier of the authorization request","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Request with the VP, presentation submission, and security parameters","required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidateAuthorizationRequestDto","description":"Request with the VP, presentation submission, and security parameters"}}}}}}},"components":{"schemas":{"ValidateAuthorizationResponseDto":{"description":"Response DTO for a validated authorization request","properties":{"code":{"type":"string","description":"The authorization code that can be exchanged for a token","minLength":1}},"required":["code"],"type":"object","additionalProperties":false},"ErrorResponseDto":{"description":"Extended error response with HTTP details","properties":{"name":{"type":"string","description":"Error name"},"message":{"type":"string","description":"Error message"},"code":{"type":"number","format":"double","description":"Error code"},"statusCode":{"type":"number","format":"double","description":"HTTP status code"},"httpCode":{"type":"number","format":"double","description":"HTTP code"},"method":{"type":"string","description":"HTTP method"},"path":{"type":"string","description":"Request path"},"stack":{"type":"string","description":"Stack trace (tylko w środowisku deweloperskim)"}},"required":["name","message","code","statusCode","httpCode","method","path"],"type":"object","additionalProperties":false},"ValidateAuthorizationRequestDto":{"description":"Request DTO for validating an authorization request","properties":{"presentation_submission":{"$ref":"#/components/schemas/PresentationSubmission","description":"The presentation submission that maps credentials to the presentation definition"},"vp_token":{"$ref":"#/components/schemas/VP","description":"The verifiable presentation containing credentials"}},"required":["presentation_submission","vp_token"],"type":"object","additionalProperties":false},"PresentationSubmission":{"properties":{"descriptor_map":{"items":{"$ref":"#/components/schemas/DescriptorMap"},"type":"array","description":"Maps between input descriptors and credentials in the presentation"},"definition_id":{"type":"string","description":"ID of the presentation definition this submission satisfies"},"id":{"type":"string","description":"Unique identifier for this submission"}},"required":["descriptor_map","definition_id","id"],"type":"object","description":"Describes how a presentation satisfies a presentation definition.\nMaps between the input descriptors in a definition and the\ncorresponding credentials in a presentation."},"DescriptorMap":{"properties":{"path_nested":{"properties":{"format":{"type":"string","description":"Format of the nested content"},"path":{"type":"string","description":"JSONPath to nested content"}},"required":["format","path"],"type":"object","description":"Optional nested path information"},"format":{"type":"string","description":"Optional format of the credential"},"path":{"type":"string","description":"JSONPath to the location of the corresponding credential in the presentation"},"id":{"type":"string","description":"ID of the input descriptor this map corresponds to"}},"required":["path","id"],"type":"object","description":"Maps between input descriptors and the corresponding credentials\nin a verifiable presentation."},"VP":{"properties":{"proof":{"$ref":"#/components/schemas/Proof","description":"Optional cryptographic proof"},"holder":{"anyOf":[{"type":"string"},{"properties":{"id":{"type":"string"}},"required":["id"],"type":"object"}],"description":"The entity that holds the presentation"},"id":{"type":"string","description":"Optional unique identifier for the presentation"},"verifiableCredential":{"items":{"$ref":"#/components/schemas/VC"},"type":"array","description":"Array of verifiable credentials included in the presentation"},"type":{"anyOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"}],"description":"Type of the verifiable presentation"},"@context":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"string"}],"description":"JSON-LD context"}},"required":["holder","verifiableCredential","type","@context"],"type":"object","description":"Represents a Verifiable Presentation that contains verifiable credentials.\n\nA Verifiable Presentation (VP) is a data model that packages Verifiable Credentials\nfor sharing with a verifier. It includes cryptographic proof of the holder's control\nover the presented credentials.\n\nThe model follows the W3C Verifiable Presentations Data Model specification and can be\nserialized as JSON-LD."},"Proof":{"properties":{"challange":{"type":"string"},"nonce":{"type":"string"},"jws":{"$ref":"#/components/schemas/Base64"},"proofValue":{"$ref":"#/components/schemas/Base64"},"proofPurpose":{"$ref":"#/components/schemas/ProofPurpose"},"verificationMethod":{"$ref":"#/components/schemas/DIDUri"},"created":{"type":"string"},"type":{"$ref":"#/components/schemas/ProofType"}},"required":["proofPurpose","verificationMethod","created","type"],"type":"object"},"Base64":{"type":"string"},"ProofPurpose":{"type":"string","enum":["assertionMethod","authentication","keyAgreement","contractAgreement","capabilityInvocation","capabilityDelegation"]},"DIDUri":{"type":"string"},"ProofType":{"type":"string","enum":["EcdsaSecp256k1Signature2019","EcdsaSecp256r1Signature2019","SchnorrSecp256k1Signature2019","Ed25519Signature2018","Ed25519Signature2020","JwtProof2020","JsonWebSignature2020"],"nullable":false},"VC":{"$ref":"#/components/schemas/VCv1_1","description":"Current version of the Verifiable Credential format used in the implementation"},"VCv1_1":{"properties":{"evidence":{"additionalProperties":false,"type":"object","description":"Additional supporting information for the claims in the credential"},"credentialStatus":{"items":{"$ref":"#/components/schemas/CredentialStatus"},"type":"array","description":"Credential status information, such as revocation or suspension"},"credentialSchema":{"$ref":"#/components/schemas/CredentialSchema","description":"Schema that defines the structure of the credential subject"},"expirationDate":{"type":"string","description":"Date and time when the credential expires"},"proof":{"$ref":"#/components/schemas/Proof","description":"Cryptographic proof that can be used to detect tampering and verify the issuer"},"credentialSubject":{"anyOf":[{"$ref":"#/components/schemas/CredentialSubject"},{"items":{"$ref":"#/components/schemas/CredentialSubject"},"type":"array"}],"description":"Claims about the subject of the credential"},"issuanceDate":{"type":"string","description":"Date and time when the credential was issued"},"issuer":{"$ref":"#/components/schemas/Issuer","description":"Entity that issued the credential"},"id":{"$ref":"#/components/schemas/URI","description":"Unique identifier for the credential"},"type":{"items":{"type":"string"},"type":"array","description":"Credential type information that defines what the credential represents"},"@context":{"anyOf":[{"items":{"$ref":"#/components/schemas/URI"},"type":"array"},{"$ref":"#/components/schemas/URI"}],"description":"JSON-LD context that defines the vocabulary of the credential"}},"required":["credentialSubject","issuanceDate","issuer","type","@context"],"type":"object","description":"Verifiable Credential format following W3C VC Data Model 1.1 specification."},"CredentialStatus":{"description":"A single entry in a bitstring-based status list, per VC-DATA-MODEL-2.0","properties":{"id":{"$ref":"#/components/schemas/URI"},"type":{"type":"string","enum":["BitstringStatusListEntry"],"nullable":false},"statusPurpose":{"anyOf":[{"$ref":"#/components/schemas/StatusPurpose"},{"type":"string"}]},"statusListIndex":{"type":"number","format":"double"},"statusListCredential":{"$ref":"#/components/schemas/URI"},"statusSize":{"type":"number","format":"double"},"statusMessage":{"items":{"properties":{"message":{"type":"string"},"status":{"type":"string"}},"additionalProperties":{},"required":["message","status"],"type":"object"},"type":"array"}},"required":["type","statusPurpose","statusListIndex","statusListCredential"],"type":"object","additionalProperties":false},"URI":{"type":"string"},"StatusPurpose":{"enum":["refresh","revocation","suspension","message"],"type":"string"},"CredentialSchema":{"properties":{"type":{"type":"string","enum":["JsonSchemaValidator2018"],"nullable":false,"description":"The type of schema validator, currently only supporting JsonSchemaValidator2018"},"id":{"$ref":"#/components/schemas/URI","description":"The identifier for the schema, typically a URI pointing to the schema definition"}},"required":["type","id"],"type":"object","description":"Represents a credential schema reference that defines the structure of claims in a credential.\nFollowing W3C VC Data Model, this provides the means to validate credential contents."},"CredentialSubject":{"$ref":"#/components/schemas/Record_string.unknown_"},"Record_string.unknown_":{"properties":{},"additionalProperties":{},"type":"object","description":"Construct a type with a set of properties K of type T"},"Issuer":{"properties":{"name":{"type":"string"},"id":{"$ref":"#/components/schemas/DIDUri"}},"required":["id"],"type":"object","description":"Normalized representation of an issuer after processing InputIssuer."}}}}
```


# Verifier

The Verifier is a core component of the EVDI (Empeiria's End-to-End Verifiable Data Infrastructure) ecosystem. It requests and validates Verifiable Presentations (VPs) from wallets and checks the Verifiable Credentials (VCs) inside them, using the **OpenID4VP** protocol over a standards-based HTTP API. Acting as the relying party between wallets and your application, the Verifier lets you build credential-based access control and passwordless login flows on top of cryptographically verifiable claims.

**Key Highlights:**

* **Standards Alignment** Implements **OpenID4VP** for presentation requests and verifies W3C-aligned Verifiable Credentials in two formats: **SD-JWT VC** (`dc+sd-jwt`), which honors selective disclosure so holders reveal only the claims you ask for, and **JWT-VC-JSON** (`jwt_vc_json`). Three OpenID4VP draft versions are supported: `v1` (default), `v1.draft21`, and `v1.draft24`.
* **Flexible Credential Queries** Request exactly the credentials and claims you need using either a **DCQL** query or a **DIF Presentation Exchange v2** definition. Each authorization request must carry exactly one of the two.
* **Multiple Response Modes** Authorization requests can use `direct_post`, `direct_post.jwt` (default), or the Digital Credentials API modes `dc_api` and `dc_api.jwt`, so you can integrate with both redirect-based wallets and in-browser credential APIs.
* **Real-Time Feedback** Each request creates a **verification session** whose state you can poll, or subscribe to over **Server-Sent Events (SSE)** to receive live updates as the wallet responds and verification completes.
* **Standalone Credential Verification** Beyond the full presentation flow, a direct endpoint verifies a single compact credential string (SD-JWT or JWT-VC) and returns its decoded claims — useful for server-to-server checks and testing.
* **Decentralized Identifiers** Issuer and holder DIDs using **`did:web`** and **`did:key`** (Ed25519) are supported today; the service serves its own `did:web` documents over HTTPS (see [Terminology and Concepts](/develop/verifier/terminology-and-concepts) for `did:empe`).
* **Security and Access Controls** Verifier endpoints require a valid **OIDC JWT Bearer token** (`Authorization: Bearer <token>`), validated against a JWKS endpoint. HTTPS is required in production.

***

## Verifying a Presentation

The typical verifier workflow is: create an authorization request that describes the credentials you need, hand the request URI (or its QR code) to a wallet, then track the verification session until it completes. All requests below require a valid `Authorization: Bearer <token>` header.

### 1. Create an Authorization Request

**`POST /verifiers/:verifierId/authorization-requests`**

**Request Body:**

```json
{
  "dcqlQuery": {
    "credentials": [
      {
        "id": "employee_badge",
        "format": "dc+sd-jwt",
        "meta": { "vct_values": ["https://issuer.example.com/vct/employee-badge"] },
        "claims": [
          { "path": ["employee_id"] },
          { "path": ["department"] }
        ]
      }
    ]
  },
  "responseMode": "direct_post.jwt",
  "version": "v1"
}
```

* **dcqlQuery**: A DCQL query describing the requested credentials and claims. Provide exactly one of `dcqlQuery` or `presentationDefinition`.
* **presentationDefinition**: A DIF Presentation Exchange v2 definition, as an alternative to `dcqlQuery`. Requires `version` to be `v1.draft21` or `v1.draft24`.
* **responseMode**: One of `direct_post`, `direct_post.jwt`, `dc_api`, or `dc_api.jwt`. Defaults to `direct_post.jwt`.
* **version**: OpenID4VP draft version — `v1`, `v1.draft21`, or `v1.draft24`. Defaults to `v1`, which supports only `dcqlQuery`; use `v1.draft21` or `v1.draft24` with `presentationDefinition` (and note that `dcqlQuery` cannot be combined with `v1.draft21`).
* **signingDid**: Optional DID used to sign the authorization request. If omitted, the `verifierId` path parameter must already be a DID.
* **expectedOrigins**: Optional list of expected origins for Digital Credentials API flows.
* **authorizationResponseRedirectUri**, **transactionData**, **verifierInfo**: Optional advanced parameters included in the request.

**Response Body:**

```json
{
  "authorizationRequestUri": "openid4vp://?request_uri=...",
  "verificationSessionId": "18b760b1-4ab8-4d38-9d42-6b4c25a4d2e0",
  "authorizationRequestId": "1b8a6a8b-2ac1-4f0b-9d9c-6b9c859b1af2",
  "expiresAt": "2024-08-09T12:34:56.789Z"
}
```

* **authorizationRequestUri**: The `openid4vp://` URI the wallet opens to respond to the request.
* **verificationSessionId**: Identifier used to track and subscribe to the verification session.
* **authorizationRequestId**: Identifier of the hosted request, when applicable.
* **expiresAt**: When the request expires, in ISO format.

To obtain a scannable QR code instead of JSON, call **`POST /verifiers/:verifierId/authorization-requests/qr-code`** with the same body. It returns a **PNG image** and exposes the request details in the `X-Authorization-Request-Uri`, `X-Verification-Session-Id`, and `X-Authorization-Request-Id` response headers.

### 2. Track the Verification Session

**`GET /verification-sessions/:sessionId`**

Returns the current session state and request details.

**Response Body:**

```json
{
  "id": "18b760b1-4ab8-4d38-9d42-6b4c25a4d2e0",
  "verifierId": "verifier-empe",
  "state": "RequestCreated",
  "authorizationRequestId": "1b8a6a8b-2ac1-4f0b-9d9c-6b9c859b1af2",
  "authorizationRequestUri": "openid4vp://?request_uri=...",
  "expiresAt": "2024-08-09T12:34:56.789Z"
}
```

* **state**: The session lifecycle state, which advances as the wallet responds (for example `RequestCreated`).
* **errorMessage**: Present when verification fails, describing the last error observed.

### 3. Subscribe to Live Updates (SSE)

**`GET /verification-sessions/:sessionId/events`**

Opens a **Server-Sent Events** stream for the session. The stream emits a `session` event carrying the session object (the same shape as the read above) immediately on subscribe — reflecting the session's current state — and again each time the state changes, plus a `ping` heartbeat every 15 seconds to keep the connection alive. Use it to update your UI in real time while the wallet presents its credentials.

***

## Standalone Credential Verification

To verify a single credential string directly — without a presentation flow — post its compact serialized form.

**`POST /credentials/verify`**

**Request Body:**

```json
{
  "credential": "eyJhbGciOiJFZERTQSJ9.eyJ2Y3QiOi...~WyJhIiwiZW1wbG95ZWVfaWQiXQ~"
}
```

* **credential**: A compact JWT or SD-JWT credential string.

**Response Body (valid):**

```json
{
  "isValid": true,
  "format": "dc+sd-jwt",
  "payload": { "vct": "https://issuer.example.com/vct/employee-badge", "employee_id": "EMP-001" },
  "prettyClaims": { "vct": "https://issuer.example.com/vct/employee-badge", "employee_id": "EMP-001", "full_name": "Ada Lovelace", "department": "Engineering" }
}
```

* **isValid**: Whether the credential passed verification.
* **format**: The detected credential format. A verified SD-JWT credential returns `"dc+sd-jwt"`; a verified JWT-VC returns `"jwt_vc"`.
* **payload**: The decoded credential payload.
* **prettyClaims**: For SD-JWT credentials, all disclosed claims merged into a single object.

**Response Body (invalid):**

```json
{
  "isValid": false,
  "error": "<reason the credential failed verification>"
}
```

***

## Authentication

All Verifier endpoints are protected. Requests must include a valid OIDC-issued JWT in the `Authorization` header:

```bash
Authorization: Bearer <token>
```

Tokens are validated against a JWKS endpoint (`OIDC_JWKS_URL`). See [Authentication](/develop/verifier/authentication) for token validation details and the local-development bypass.


# Terminology and Concepts

This glossary defines the terms used throughout the Verifier documentation.

* **Self-Sovereign Identity (SSI)**: A user-centric approach to digital identity, allowing individuals and organizations to control their identifiers and credentials without centralized authorities.
* **Decentralized Identifier (DID)**: A unique identifier whose DID Document (public keys and service endpoints) can be resolved to establish trust. In this system, DIDs are either `did:web` (resolved over HTTPS at a domain) or `did:key` (self-contained — the public key is encoded directly in the identifier). A verifier signs its authorization requests with one of these DIDs so that wallets can verify the request originates from a known party.
* **Verifiable Credential (VC)**: A cryptographically signed credential that attests to certain attributes of an entity. VCs follow W3C standards, ensuring integrity and authenticity.
* **Verifiable Presentation (VP)**: A presentation of one or more VCs by a holder to a verifier. The VP proves possession and authenticity of the credentials without revealing unnecessary information.
* **Credential formats**: The Verifier accepts two credential formats: **SD-JWT VC** (`dc+sd-jwt`), which supports **selective disclosure** so a holder can reveal only the claims a verifier needs, and **JWT-VC-JSON** (`jwt_vc_json`), a JWT-based W3C credential.
* **OpenID4VP (OpenID for Verifiable Presentations)**: The protocol the Verifier uses to request, receive, and validate presentations. You create an authorization request, the holder's wallet returns a presentation, and the Verifier validates it. Supported draft versions are `v1`, `v1.draft21`, and `v1.draft24`.
* **Verification queries (DCQL / Presentation Exchange)**: The two ways to express which credentials and which fields a presentation must contain. **DCQL** (Digital Credentials Query Language) and **DIF Presentation Exchange v2** are both supported — each authorization request carries exactly one of them. They act as structured filters, ensuring that only credentials meeting the defined criteria are accepted.
* **EMPE DID method / EMPE blockchain**: An on-chain DID method in the wider Empeiria EVDI ecosystem. The Verifier resolves `did:web`, `did:key`, and `did:jwk` directly; `did:empe` resolution is an ecosystem roadmap item (see [Future Enhancements](/develop/verifier/future-enhancements)).


# Authentication

This page explains how to authenticate calls to the Verifier API. Every non-public endpoint is protected by an **OIDC JWT Bearer token**: you obtain a token from your identity provider, send it in the `Authorization` header, and the Verifier validates it against a JWKS endpoint before serving the request. The only credential the API accepts is a signed, unexpired JWT — there is no client secret or per-verifier shared secret to manage.

***

## Overview

* The Verifier validates an incoming token's signature against the JSON Web Key Set (JWKS) published by your OIDC provider (for example Keycloak, Auth0, or any compliant issuer). The JWKS location is configured on the service through the `OIDC_JWKS_URL` environment variable.
* Authentication is **stateless**: the service does not maintain login sessions for the API. Each request must carry its own token, and the token's claims identify the caller.
* Most Verifier endpoints — creating authorization requests, reading verification sessions, and verifying a single credential — require **only a valid token, with no specific role**. The `admin` role is enforced separately on the agent-management routes (`/agent/*`) used to register a verifier and on cloud-wallet routes (`/holders/*`).
* For local development, see [Development mode](#development-mode).

***

## Sending the Authorization header

Attach the token to every protected request as a Bearer credential:

```
Authorization: Bearer <your-jwt>
```

A typical call from a backend looks like this:

```typescript
const VERIFIER_BASE_URL = 'https://your-verifier.evdi.app';

async function verifyCredential(token: string, credential: string) {
  const res = await fetch(`${VERIFIER_BASE_URL}/credentials/verify`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${token}`,
    },
    body: JSON.stringify({ credential }),
  });

  return res.json();
}
```

The same header applies to the authorization-request and verification-session endpoints. If the header is missing, malformed, or carries an expired token, the request is rejected with `401 Unauthorized` before any business logic runs.

***

## Token validation

When a request arrives with a Bearer token, the Verifier performs the following checks:

* **Signature** — the token is verified with the `RS256` algorithm. The service reads the `kid` from the JWT header and selects the matching public key from the JWKS published at `OIDC_JWKS_URL`. A token whose header has no `kid`, or whose `kid` does not match any published key, is rejected.
* **Expiration** — the `exp` claim is enforced; expired tokens are rejected.
* **Key caching** — fetched signing keys are cached for roughly 10 minutes, and JWKS requests are rate-limited to 10 per minute, so the service does not hit your identity provider on every call.

A request that fails any of these checks receives a `401 Unauthorized` response in the standard error shape:

```json
{
  "statusCode": 401,
  "message": "Unauthorized",
  "path": "/credentials/verify",
  "timestamp": "2026-06-22T12:34:56.789Z"
}
```

Because validation relies on the JWKS, your OIDC provider can rotate its signing keys without any change to the Verifier — the new key is picked up automatically once the cache expires.

***

## Roles and access control

The Verifier reads roles from the token's claims and applies them per route group:

* **Verifier endpoints** require **only a valid token**. The authorization-request routes (`/verifiers/{verifierId}/authorization-requests`), the verification-session routes (`/verification-sessions/{sessionId}`), and the direct verification route (`/credentials/verify`) carry no role requirement — any caller with a valid JWT may use them.
* **Agent-management endpoints** under `/agent/*` — including `POST /agent/verifier`, which you call once to register a verifier — additionally require the **`admin`** role. The role is read from the token's `realm_access.roles` array (client roles in `resource_access.<client>.roles` are also accepted). A valid token without the `admin` role is rejected with `403 Forbidden`.

In practice this means you need an `admin`-roled token to set up a verifier, but the day-to-day verification calls only need an authenticated token. Restrict who can obtain `admin` tokens in your identity provider, and scope the tokens your application uses to the minimum it needs.

A typical token payload that satisfies the `admin` requirement looks like this:

```json
{
  "sub": "service-account-verifier",
  "exp": 1782345296,
  "realm_access": {
    "roles": ["admin"]
  }
}
```

***

## Obtaining a token

The Verifier does not issue tokens; you request them from your OIDC provider. For server-to-server integrations the **client-credentials grant** is the usual choice — it yields a token tied to a service account rather than an interactive user:

```typescript
async function getAccessToken(): Promise<string> {
  const res = await fetch(
    'https://auth.example.com/realms/empe/protocol/openid-connect/token',
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: new URLSearchParams({
        grant_type: 'client_credentials',
        client_id: 'your-client-id',
        client_secret: 'your-oidc-client-credentials-secret',
      }),
    },
  );

  const data = await res.json();
  return data.access_token;
}
```

The `client_secret` above is the credential **your application** presents to the **OIDC provider** to obtain a token — it is never sent to the Verifier, which only ever sees the resulting JWT in the `Authorization` header.

> For a **One-Click deployment**, obtain the token from your realm's Keycloak token endpoint — `https://<keycloak-host>/realms/<realm>/protocol/openid-connect/token` — using the credentials shown when you created the deployment; see [Authenticating to Your Deployment](/develop/intro/authenticating-to-your-deployment). If you run your **own identity provider**, use its token endpoint and a client you control (as shown above).

***

## Development mode

To make local testing easier, the service supports a development bypass. Setting `AUTH_DISABLED=true` skips token validation entirely and treats every request as an authenticated `admin` user:

```bash
AUTH_DISABLED=true
```

With this flag set, you can omit the `Authorization` header completely and call any endpoint, including the `/agent/*` routes. When authentication is disabled, `OIDC_JWKS_URL` is not required. Because `AUTH_DISABLED=true` removes all access control, use it only for local development and never in any deployed or shared environment.

***

## Best practices

* **Always use HTTPS.** Tokens are bearer credentials — anyone who captures one can act as you until it expires — and OpenID4VP requires HTTPS in production regardless.
* **Keep tokens server-side, short-lived, and cached.** Obtain and store tokens in your backend (never in browser or mobile clients), and refresh them automatically shortly before `exp` rather than on every call, so a leaked token has a limited window of usefulness.
* **Scope roles tightly.** Reserve the `admin` role for the one-time verifier setup, and use lower-privilege tokens for ongoing verification traffic.
* **Account for rate limiting.** The service applies a global limit of 60 requests per 60 seconds; design your retry and refresh logic with this in mind.

***

## Related pages

* [Verifier Client Configuration](/develop/verifier/client-configuration) — end-to-end server-side integration, including registering a verifier and creating authorization requests with the Bearer token.
* [Security Considerations](/develop/verifier/security-considerations) — the full security model around transport, request binding, and credential integrity.


# Architecture Overview

The Verifier is the **OpenID4VP capability of the EVDI SSI service**, exposed as a set of REST and Server-Sent Events (SSE) endpoints. Your application drives a verification by calling these endpoints, rendering the resulting QR code or deep link, and reacting to session state as it changes. This keeps the integration thin: everything is plain HTTP calls plus an SSE subscription, with no library or separate microservice to deploy.

**Key Components:**

1. **Verifier Endpoints**:
   * Create OpenID4VP authorization requests scoped to a verifier (`POST /verifiers/:verifierId/authorization-requests`), describing the credentials you want with a **DCQL** query or a **DIF Presentation Exchange v2** definition.
   * Generate a ready-to-scan **PNG QR code** for the same request (`POST /verifiers/:verifierId/authorization-requests/qr-code`).
   * Track a **verification session** server-side and let you read it on demand (`GET /verification-sessions/:sessionId`).
   * Stream live session updates over **SSE** (`GET /verification-sessions/:sessionId/events`).
   * Verify a standalone credential string without a presentation flow (`POST /credentials/verify`).
2. **Empe DID Wallet**:
   * A user's personal wallet application that holds their DIDs and Verifiable Credentials.
   * Scans the QR code or opens the `openid4vp://` request URI to fetch the authorization request.
   * Selects matching credentials and submits a **Verifiable Presentation** back to the Verifier endpoints.
3. **Front-End Integration**:
   * Your application renders the QR code (or deep link) returned when you create the authorization request.
   * It subscribes to the session's SSE stream and updates the UI as the session moves through its lifecycle, showing the final outcome to the user in real time.
4. **Your Backend (Relying Party)**:
   * Calls the Verifier endpoints with a valid OIDC **Bearer** token (`Authorization: Bearer <token>`), validated against a JWKS endpoint (see [Authentication](/develop/verifier/authentication)).
   * Owns all business logic. The service does **not** issue application JWTs or sessions for you — when a verification session reaches the `ResponseVerified` state, your backend decides what that means (log the user in, grant access, create your own session token, and so on).
5. **Trust Roots**:
   * Issuer and holder identities are **Decentralized Identifiers (DIDs)** resolved directly by the service. `did:web` is resolved over HTTPS from the DID's origin; `did:key` (and `did:jwk`) are self-describing and need no external lookup. `did:web` documents created through EVDI are hosted by the service itself.
   * Authorization requests are themselves **DID-signed** using the verifier's verification method, so wallets can authenticate the requester.
   * Verification covers cryptographic signatures, the credential format, and the presentation itself. Revocation and status-list checks are not part of verification today (see [Future Enhancements](/develop/verifier/future-enhancements)).

## Verification Session Lifecycle

Every authorization request you create is backed by a verification session whose `state` you can poll or stream. The states are:

* **`RequestCreated`**: The authorization request has been created and is waiting to be retrieved by a wallet.
* **`RequestUriRetrieved`**: A wallet has fetched the request via its `request_uri`.
* **`ResponseVerified`**: The wallet submitted a presentation and the service verified it successfully. This is your signal to apply business logic.
* **`Error`**: The presentation was rejected or the flow failed; `errorMessage` on the session describes why.

Sessions also carry an `expiresAt` timestamp. Reading a session returns its full record:

```json
{
  "id": "18b760b1-4ab8-4d38-9d42-6b4c25a4d2e0",
  "verifierId": "did:web:verifier.example.com:8f2c...",
  "state": "ResponseVerified",
  "authorizationRequestId": "1b8a6a8b-2ac1-4f0b-9d9c-6b9c859b1af2",
  "authorizationRequestUri": "openid4vp://?request_uri=...",
  "expiresAt": "2026-06-22T12:34:56.789Z"
}
```

The SSE stream at `GET /verification-sessions/:sessionId/events` emits a `session` event (carrying the record above) on every state change, plus a `ping` heartbeat roughly every 15 seconds to keep the connection alive. Subscribing to this stream is the recommended way to drive a responsive front-end rather than polling the session endpoint.

This layered design keeps a clear separation of concerns: the service handles request signing, session tracking, and presentation verification against open standards, while your application owns presentation of the QR code, the user experience, and what happens after a credential is verified.


# Core Responsibilities

The Verifier's responsibilities fall into five areas:

1. **Verification Request Management**:
   * Creates OpenID4VP authorization requests, choosing a response mode (`direct_post`, `direct_post.jwt`, `dc_api`, or `dc_api.jwt`; default `direct_post.jwt`) and an OpenID4VP draft version (`v1`, `v1.draft21`, or `v1.draft24`; default `v1`).
   * Specifies the required credentials and attributes through a configurable query language — either a **DCQL** query or a **DIF Presentation Exchange v2** definition. Exactly one of the two must be supplied per request.
   * Signs each authorization request with the verifier's DID, so wallets can authenticate the relying party before disclosing any data. The signing DID is taken from the `verifierId` when it is itself a DID, or from the optional `signingDid` field in the request body.
   * Returns an authorization-request URI (`openid4vp://...`) and a verification session id, and can render the same request as a scannable QR code.
2. **Credential Validation**:
   * Checks the cryptographic signatures of presented credentials, supporting **SD-JWT VC** and **JWT-VC** formats.
   * Resolves the issuer's DID document to confirm the signing key and the issuer's identity. Issuer DIDs are resolved over standard methods — `did:web` (served over an HTTPS origin) and `did:key` — rather than through any centralized registry.
   * Honors **SD-JWT selective disclosure**, so only the claims the holder chooses to reveal are processed.
   * Validation today rests on DID resolution and signature verification; revocation-status and domain-linkage checks are planned additions (see [Future Enhancements](/develop/verifier/future-enhancements)).
3. **Real-Time Communication and Feedback**:
   * Streams verification session updates over **Server-Sent Events (SSE)**, so your application learns about progress and the final outcome as it happens.
   * Emits a `session` event carrying the current session state immediately on subscribe and on each subsequent change, plus a `ping` keepalive every 15 seconds to hold the connection open.
4. **Session Management**:
   * Tracks each verification session through its lifecycle — `RequestCreated` → `RequestUriRetrieved` → `ResponseVerified`, or `Error` if the response fails validation. Sessions also carry an `expiresAt` timestamp after which the request is no longer accepted.
   * Exposes the session state (by polling `GET /verification-sessions/:sessionId` or by subscribing to its SSE stream) so your integration can trigger its own login or access-control logic once a session reaches `ResponseVerified`. The Verifier itself does not issue application tokens or sessions — your relying party decides what a successful verification grants.
5. **Integration Support**:
   * Provides a REST + SSE surface for the full presentation flow: create an authorization request (with optional QR-code rendering), then read or watch the resulting verification session.
   * Offers a standalone **`POST /credentials/verify`** endpoint for ad-hoc credential checks outside the presentation flow — useful when you already hold a compact SD-JWT or JWT-VC string and simply want to confirm its validity. The response reports `isValid`, the detected `format`, and the decoded `payload` (plus `prettyClaims` for SD-JWT), or an `error` message when the credential is invalid.

For example, a standalone credential check:

```json
// POST /credentials/verify
{
  "credential": "eyJhbGciOiJFZERTQSJ9.eyJ2Y3QiOiJodHRwczovL2lzc3Vlci5leGFtcGxlLmNvbS92Y3QvZW1wbG95ZWUtYmFkZ2UiLCJlbXBsb3llZV9pZCI6IkUtMTAyNCJ9.signature~WyJzYWx0IiwiZnVsbF9uYW1lIiwiQWRhIExvdmVsYWNlIl0~WyJzYWx0IiwiZGVwYXJ0bWVudCIsIkVuZ2luZWVyaW5nIl0~"
}
```

A valid SD-JWT VC returns:

```json
{
  "isValid": true,
  "format": "dc+sd-jwt",
  "payload": { "vct": "https://issuer.example.com/vct/employee-badge", "iss": "did:web:issuer.example.com:...", "iat": 1718000000, "employee_id": "E-1024" },
  "prettyClaims": {
    "vct": "https://issuer.example.com/vct/employee-badge",
    "employee_id": "E-1024",
    "full_name": "Ada Lovelace",
    "department": "Engineering"
  }
}
```

The `format` field reports the detected credential format: `dc+sd-jwt` for a verified SD-JWT VC, and `jwt_vc` for a verified JWT-VC.

In every flow, the relying party stays in control of what a verified presentation unlocks.


# Query Language

When the Verifier creates an authorization request, it must describe exactly which credentials it will accept and which fields it needs from them. You express those requirements using one of two standard query forms — a **DCQL** (Digital Credentials Query Language) query or a **DIF Presentation Exchange v2** definition — supplied inline in the request body. The wallet uses your query to select matching credentials and, for SD-JWT credentials, to disclose only the requested claims.

## Key Concepts

* **Query forms**: Credential requirements are expressed as either a `dcqlQuery` object or a `presentationDefinition` object. Both are standard formats, so wallets that follow the OpenID4VP specification understand them without any vendor-specific extensions.
* **Where the query lives**: The query is not stored or referenced by ID — it is sent inline in the body of `POST /verifiers/:verifierId/authorization-requests` (and its `/qr-code` variant). Each request carries its own query.
* **One form per request**: Supply exactly one of `dcqlQuery` or `presentationDefinition`. If you omit both, the request is rejected with `400 Bad Request` and the message `Either dcqlQuery or presentationDefinition must be provided.` Sending both passes the service's own input validation but fails downstream for every `version` value, surfacing as a `500`.
* **Paths and filters** (Presentation Exchange): Inside a Presentation Exchange input descriptor, `constraints.fields` use JSONPath expressions in `path` to locate a value in the credential, and an optional JSON Schema (Draft 7) `filter` to validate it (for example `type`, `enum`, `pattern`, or `contains`).
* **Selective disclosure**: For SD-JWT VC credentials, the claims your query asks for are the claims the wallet selectively discloses. Fields the credential supports but the query does not request stay hidden, so the holder shares only what the Verifier needs.

## DCQL Example

DCQL is a compact, credential-centric query format. The following query requests an `EmployeeBadge` SD-JWT VC and asks the wallet to disclose only the `employee_id` and `department` claims:

```json
{
  "responseMode": "direct_post.jwt",
  "version": "v1",
  "dcqlQuery": {
    "credentials": [
      {
        "id": "employee_badge",
        "format": "dc+sd-jwt",
        "meta": {
          "vct_values": ["https://issuer.example.com/vct/employee-badge"]
        },
        "claims": [
          { "path": ["employee_id"] },
          { "path": ["department"] }
        ]
      }
    ]
  }
}
```

* `format` is the requested credential format (for example `dc+sd-jwt` for SD-JWT VC).
* `meta.vct_values` restricts the request to credentials with a matching `vct` (verifiable credential type).
* Each entry in `claims` names a claim with a `path`, and those are the claims the wallet discloses.

## Presentation Exchange Example

A DIF Presentation Exchange v2 definition is more verbose but lets you attach JSON Schema filters to individual fields. `presentationDefinition` requires `version` `v1.draft21` or `v1.draft24` (the default `v1` supports only `dcqlQuery`). The following definition requests a credential whose `type` array contains `EmploymentCredential` and whose subject is identified by a `did:web` identifier:

```json
{
  "responseMode": "direct_post.jwt",
  "version": "v1.draft24",
  "presentationDefinition": {
    "id": "pd-employment",
    "input_descriptors": [
      {
        "id": "employment_vc",
        "format": { "jwt_vc_json": { "alg": ["EdDSA"] } },
        "constraints": {
          "fields": [
            {
              "path": ["$.type"],
              "filter": {
                "type": "array",
                "contains": { "const": "EmploymentCredential" }
              }
            },
            {
              "path": ["$.credentialSubject.id"],
              "filter": {
                "type": "string",
                "pattern": "^did:web:.*$"
              }
            }
          ]
        }
      }
    ]
  }
}
```

* `path` holds one or more JSONPath expressions; the first one that resolves is used.
* `filter` is a JSON Schema (Draft 7) the extracted value must satisfy. Use a method-agnostic pattern (for example `^did:.*$`) when you do not need to constrain the DID method.

## Response

A successful request returns the authorization request URI and the verification session you can poll or stream for results:

```json
{
  "authorizationRequestUri": "openid4vp://?request_uri=...",
  "verificationSessionId": "18b760b1-4ab8-4d38-9d42-6b4c25a4d2e0",
  "authorizationRequestId": "1b8a6a8b-2ac1-4f0b-9d9c-6b9c859b1af2",
  "expiresAt": "2024-08-09T12:34:56.789Z"
}
```

Pick whichever form fits your stack — both let you constrain credential types, claims, and field values.


# Client Configuration

This page explains how to integrate the Verifier into a backend application by calling its REST API directly. The Verifier exposes endpoints for registering a verifier, creating OpenID4VP authorization requests, and tracking the resulting verification sessions. Your server authenticates with an OIDC bearer token and talks to the API over HTTPS — no client library is required.

The examples below use TypeScript with `fetch` running inside an Express server, but the same calls work from any HTTP client.

***

## Prerequisites

Before issuing any verification request, make sure you have the following:

* **The Verifier base URL** — the HTTPS origin where the Verifier API is served (for example `https://your-verifier.evdi.app`). All paths below are relative to this origin.
* **An OIDC access token** — every non-public endpoint requires a valid JWT passed as `Authorization: Bearer <token>`. The token is validated against the configured JWKS endpoint of your OIDC provider (such as Keycloak). The `/agent/*` routes used to register a verifier additionally require the `admin` role; the authorization-request and verification-session routes require only a valid token.
* **A `verifierId`** — the identifier of a verifier record you create once (see below) and then reuse for all authorization requests.

For local development without tokens, see the development bypass in [Authentication](/develop/verifier/authentication#development-mode).

### Obtaining an access token

Request a token from your OIDC provider using the client-credentials grant (or any flow that yields a token carrying the `admin` role), then attach it to every call:

```typescript
async function getAccessToken(): Promise<string> {
  const res = await fetch('https://auth.example.com/realms/empe/protocol/openid-connect/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'client_credentials',
      client_id: 'your-client-id',
      client_secret: 'your-oidc-client-credentials-secret',
    }),
  });

  const data = await res.json();
  return data.access_token;
}
```

> For a **One-Click deployment**, obtain the token from your realm's Keycloak token endpoint — `https://<keycloak-host>/realms/<realm>/protocol/openid-connect/token` — using the credentials shown when you created the deployment; see [Authenticating to Your Deployment](/develop/intro/authenticating-to-your-deployment). If you run your **own identity provider**, use its token endpoint and a client you control (as shown above).

***

## Registering a Verifier

A verifier record is created once and reused. Create it with the Agent API.

**`POST /agent/verifier`**

### Request Body

* **`verifierId`** (string, optional) A custom identifier for tracing (for example `verifier-empe`). If you omit it, the service assigns one. You do not need to supply a DID here; the signing DID is chosen later when you create authorization requests.

```typescript
const VERIFIER_BASE_URL = 'https://your-verifier.evdi.app';

async function createVerifier(token: string): Promise<string> {
  const res = await fetch(`${VERIFIER_BASE_URL}/agent/verifier`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${token}`,
    },
    body: JSON.stringify({ verifierId: 'verifier-empe' }),
  });

  const data = await res.json();
  return data.verifierId;
}
```

### Response Body

* **`verifierId`** (string) The identifier to use in subsequent authorization-request paths.
* **`clientMetadata`** (object, optional) OpenID4VP client metadata attached to the verifier.

Persist the returned `verifierId` in your configuration; you will reference it in every authorization request.

***

## Creating an Authorization Request

To ask a wallet to present credentials, create an authorization request under your verifier. The request describes which credentials and claims you want, using either a **DCQL query** or a **DIF Presentation Exchange v2** definition.

**`POST /verifiers/{verifierId}/authorization-requests`**

### Request Body

Exactly one of `dcqlQuery` or `presentationDefinition` is required; the remaining fields are optional.

* **`dcqlQuery`** (object) — a DCQL query describing the requested credential configurations and claims. Required if `presentationDefinition` is not provided.
* **`presentationDefinition`** (object) — a DIF Presentation Exchange v2 definition. Required if `dcqlQuery` is not provided. Can only be used with `version` `v1.draft21` or `v1.draft24`.
* **`responseMode`** (string, optional) — one of `direct_post`, `direct_post.jwt`, `dc_api`, `dc_api.jwt`. Defaults to `direct_post.jwt`.
* **`version`** (string, optional) — OpenID4VP draft version: `v1`, `v1.draft21`, or `v1.draft24`. Defaults to `v1`, which supports only `dcqlQuery`.
* **`signingDid`** (string, optional) — the DID that signs the authorization request. If omitted, the `verifierId` in the path must itself be a DID (for example a `did:web` or `did:key` registered with the Agent API).
* **`authorizationResponseRedirectUri`** (string, optional) — a redirect URI to include in authorization responses.
* **`expectedOrigins`** (string array, optional) — expected origins for Digital Credentials API flows.
* **`transactionData`** (object array, optional) — transaction data entries to include in the request.
* **`verifierInfo`** (object array, optional) — verifier attestations to include in the request.

The following example requests an `EmployeeBadge` SD-JWT credential and asks the wallet to disclose only the `employee_id` and `department` claims:

```typescript
async function createAuthorizationRequest(token: string, verifierId: string) {
  const res = await fetch(
    `${VERIFIER_BASE_URL}/verifiers/${verifierId}/authorization-requests`,
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${token}`,
      },
      body: JSON.stringify({
        responseMode: 'direct_post.jwt',
        version: 'v1',
        signingDid: 'did:web:verifier.example.com:1f8a6a8b',
        dcqlQuery: {
          credentials: [
            {
              id: 'employee_badge',
              format: 'dc+sd-jwt',
              meta: {
                vct_values: ['https://issuer.example.com/vct/employee-badge'],
              },
              claims: [{ path: ['employee_id'] }, { path: ['department'] }],
            },
          ],
        },
      }),
    },
  );

  return res.json();
}
```

### Response Body

* **`authorizationRequestUri`** (string) — an `openid4vp://...` URI. Encode it into a QR code or use it as a deep link so a wallet can fetch and respond to the request.
* **`verificationSessionId`** (string) — the session identifier you poll or subscribe to in order to learn the outcome.
* **`authorizationRequestId`** (string, optional) — the identifier of the hosted request.
* **`expiresAt`** (string, optional) — ISO timestamp after which the request is no longer valid.

```json
{
  "authorizationRequestUri": "openid4vp://?request_uri=https://your-verifier.evdi.app/...",
  "verificationSessionId": "18b760b1-4ab8-4d38-9d42-6b4c25a4d2e0",
  "authorizationRequestId": "1b8a6a8b-2ac1-4f0b-9d9c-6b9c859b1af2",
  "expiresAt": "2026-06-22T12:34:56.789Z"
}
```

If you would rather receive a ready-to-render QR image, send the same body to **`POST /verifiers/{verifierId}/authorization-requests/qr-code`**. It returns a PNG and exposes the same values through the `X-Authorization-Request-Uri`, `X-Verification-Session-Id`, and `X-Authorization-Request-Id` response headers.

***

## Reacting to Results

The response from a wallet is processed asynchronously, so your server watches the verification session and applies its own logic (issuing a login token, completing a checkout, granting access) once the session succeeds. There is no callback framework: you own this logic.

A session moves through these states:

* **`RequestCreated`** — the request exists and is waiting for a wallet.
* **`RequestUriRetrieved`** — a wallet has fetched the request.
* **`ResponseVerified`** — the wallet returned a valid presentation. This is the success state.
* **`Error`** — verification failed; inspect `errorMessage`.

### Polling the session

**`GET /verification-sessions/{sessionId}`**

```typescript
async function getSession(token: string, sessionId: string) {
  const res = await fetch(`${VERIFIER_BASE_URL}/verification-sessions/${sessionId}`, {
    headers: { Authorization: `Bearer ${token}` },
  });
  return res.json();
}
```

The response includes the session `state`, the `verifierId`, the authorization request URI, `expiresAt`, and an `errorMessage` when something went wrong:

```json
{
  "id": "18b760b1-4ab8-4d38-9d42-6b4c25a4d2e0",
  "verifierId": "verifier-empe",
  "state": "ResponseVerified",
  "authorizationRequestId": "1b8a6a8b-2ac1-4f0b-9d9c-6b9c859b1af2",
  "authorizationRequestUri": "openid4vp://?request_uri=...",
  "expiresAt": "2026-06-22T12:34:56.789Z"
}
```

When `state` reaches `ResponseVerified`, run your post-verification logic:

```typescript
const session = await getSession(token, sessionId);

if (session.state === 'ResponseVerified') {
  // The wallet presented a valid credential. Apply your own logic here, e.g.
  // mint a session/login token, mark the user authenticated, redirect, etc.
} else if (session.state === 'Error') {
  // Verification failed.
  console.error('Verification failed:', session.errorMessage);
}
```

### Subscribing with Server-Sent Events

To avoid polling, subscribe to live updates instead.

**`GET /verification-sessions/{sessionId}/events`**

This endpoint streams Server-Sent Events. It emits a `session` event carrying the full session object immediately on subscribe (reflecting the current state) and again on every state change, plus a `ping` heartbeat every 15 seconds to keep the connection alive. Close the stream once you observe a terminal state (`ResponseVerified` or `Error`).

```typescript
import { EventSource } from 'eventsource';

const es = new EventSource(
  `${VERIFIER_BASE_URL}/verification-sessions/${sessionId}/events`,
  { fetch: (url, init) => fetch(url, { ...init, headers: { ...init.headers, Authorization: `Bearer ${token}` } }) },
);

es.addEventListener('session', (event) => {
  const session = JSON.parse(event.data);
  if (session.state === 'ResponseVerified') {
    // Apply your post-verification logic, then close the stream.
    es.close();
  } else if (session.state === 'Error') {
    console.error('Verification failed:', session.errorMessage);
    es.close();
  }
});
```

This pattern lets your backend react the moment a wallet completes the presentation, without holding open long-running poll loops.


# Frontend Integration

The front-end integration pattern is: your backend asks the Verifier to create an authorization request and returns a QR code plus a verification session id, your front-end renders the QR code and subscribes to a live status stream **served by your own backend**, and you finalize the user session once the credential has been verified.

All Verifier endpoints — including the session route `GET /verification-sessions/{sessionId}` and the Server-Sent Events (SSE) stream at `GET /verification-sessions/{sessionId}/events` — require an OIDC JWT **Bearer** token that must stay on your backend. Because a browser `EventSource` cannot send an `Authorization` header, your backend subscribes to the Verifier's SSE stream (or polls the session) and relays updates to the browser over your own origin.

## Typical Workflow

1. **Initiate Verification**:\
   Trigger a flow from the front-end (e.g., "Login with Credential"). The front-end calls your backend, which creates the authorization request on the Verifier.
2. **Create the Authorization Request**:\
   Your backend sends a `POST` to **`/verifiers/{verifierId}/authorization-requests/qr-code`** with a Bearer token. The response is a PNG image of the QR code. The verification session id is returned in the `X-Verification-Session-Id` response header, and the raw request URI in `X-Authorization-Request-Uri`.
3. **Display QR Code**:\
   Your backend forwards the PNG (and the session id) to the front-end, which renders it. Users scan the QR code with their Empe DID Wallet.
4. **Subscribe for Live Status (via your backend)**:\
   Once the QR code is displayed, the front-end opens a connection to a **proxy route on your own origin** (for example `/my-backend/verification/{sessionId}/events`), which relays the Verifier's session updates as described above.
5. **Wallet Interaction & Finalize**:\
   The wallet retrieves the request, selects matching credentials, and submits a Verifiable Presentation. When the session reaches the `ResponseVerified` state, finalize the user session — redirect the user, show their dashboard, or grant access tokens as needed.

## Creating the Request (backend)

Send the request from your backend so the Bearer token is never exposed to the browser. The body must contain exactly one of `dcqlQuery` or `presentationDefinition`. The QR-code endpoint returns a PNG (`image/png`), so read the metadata from the response **headers** rather than the body:

```javascript
// Runs on your backend
const res = await fetch(
  `${VERIFIER_BASE_URL}/verifiers/${verifierId}/authorization-requests/qr-code`,
  {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${accessToken}`,
    },
    body: JSON.stringify({
      dcqlQuery: {
        credentials: [
          {
            id: 'employee_badge',
            format: 'dc+sd-jwt',
            meta: { vct_values: ['https://issuer.example.com/vct/employee-badge'] },
            claims: [{ path: ['full_name'] }, { path: ['department'] }],
          },
        ],
      },
    }),
  },
);

const sessionId = res.headers.get('X-Verification-Session-Id');
const requestUri = res.headers.get('X-Authorization-Request-Uri');
const qrPng = await res.arrayBuffer(); // PNG bytes to forward to the front-end
```

Return the PNG bytes (or a data URL) and `sessionId` to your front-end. If you prefer to render your own QR code, use the JSON endpoint **`POST /verifiers/{verifierId}/authorization-requests`** instead — it returns `{ authorizationRequestUri, verificationSessionId, authorizationRequestId, expiresAt }`.

## Rendering the QR Code (front-end)

Render the PNG returned by your backend, for example as an object URL from a blob:

```javascript
const blob = await fetch('/my-backend/verification/start').then((r) => r.blob());
document.getElementById('qr-code').src = URL.createObjectURL(blob);
// sessionId is delivered alongside the image by your backend (e.g. a custom header or JSON)
```

## Relaying Session Updates (backend)

Expose a proxy route on **your own backend** that holds the token, subscribes to the Verifier's stream, and forwards each update to the browser. The Verifier stream emits two kinds of events:

* **`session`** — carries the verification session object, including a `state` field.
* **`ping`** — a keepalive heartbeat (data `"keepalive"`) sent every 15 seconds. It only keeps the connection open; you can drop it or forward it as-is.

The session `state` transitions through these values:

* **`RequestCreated`** — the request exists and is waiting to be retrieved.
* **`RequestUriRetrieved`** — the wallet has fetched the request and is preparing a presentation.
* **`ResponseVerified`** — the presentation was received and verified successfully. This is your success signal.
* **`Error`** — verification failed; inspect `errorMessage` for the reason.

A backend relay, for example with `EventSource` server-side (or any HTTP client that streams the response), subscribes with the Bearer token and re-emits updates to the browser over your own origin:

```javascript
// Runs on your backend — e.g. Express handler for GET /my-backend/verification/:sessionId/events
import { EventSource } from 'eventsource'; // server-side EventSource that supports custom headers

app.get('/my-backend/verification/:sessionId/events', (req, res) => {
  const { sessionId } = req.params;

  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');
  res.flushHeaders();

  // Subscribe to the Verifier with the Bearer token (never sent to the browser).
  const upstream = new EventSource(
    `${VERIFIER_BASE_URL}/verification-sessions/${sessionId}/events`,
    { fetch: (url, init) => fetch(url, { ...init, headers: { ...init.headers, Authorization: `Bearer ${accessToken}` } }) },
  );

  upstream.addEventListener('session', (event) => {
    res.write(`event: session\ndata: ${event.data}\n\n`);
  });

  // Forward the heartbeat so the browser connection stays open.
  upstream.addEventListener('ping', () => {
    res.write(`event: ping\ndata: keepalive\n\n`);
  });

  req.on('close', () => upstream.close());
});
```

If you prefer not to keep a long-lived upstream connection open, your backend can instead poll **`GET /verification-sessions/{sessionId}`** with the Bearer token and push the state to the browser; the response carries the same `state` field.

## Listening for Session Updates (front-end)

In the browser, open an `EventSource` against **your own backend proxy route** (not the Verifier). The browser sees the same `session` and `ping` events your backend forwards. Branch on the event type, then on the session state:

```javascript
// Runs in the browser — points at YOUR origin, not VERIFIER_BASE_URL
const eventSource = new EventSource(
  `/my-backend/verification/${sessionId}/events`,
);

eventSource.addEventListener('session', (event) => {
  const session = JSON.parse(event.data);

  if (session.state === 'ResponseVerified') {
    eventSource.close();
    // Verification succeeded — tell your backend to finalize the user session.
    finalizeLogin(sessionId);
  } else if (session.state === 'Error') {
    eventSource.close();
    alert(`Verification failed: ${session.errorMessage ?? 'unknown error'}`);
  }
  // RequestCreated / RequestUriRetrieved are intermediate — keep waiting.
});

// Heartbeat; no action required.
eventSource.addEventListener('ping', () => {});

eventSource.onerror = () => {
  // Reconnect or surface a "connection lost" state as appropriate.
};
```

A `session` event is emitted immediately on subscribe at the Verifier, so once your relay forwards it your handler sees the current state right away. The session object also includes `id`, `verifierId`, `authorizationRequestUri`, `expiresAt`, and `errorMessage`.

## Finalizing the Session

When the front-end observes `ResponseVerified`, it should **not** trust that signal alone to grant access. Instead, call your own backend, which independently confirms the result by reading **`GET /verification-sessions/{sessionId}`** (with a Bearer token) and checking that `state` is `ResponseVerified` before issuing your application's session token or redirecting the user. This keeps the trust boundary on your server.

```javascript
async function finalizeLogin(sessionId) {
  const ok = await fetch('/my-backend/verification/finalize', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ sessionId }),
  }).then((r) => r.ok);

  if (ok) window.location.href = '/dashboard';
}
```

In short: the browser only ever sees the QR code and status updates; the Bearer token, the SSE subscription, and the access decision stay on your backend.


# Server-Side VP Queries

This page explains how the Verifier expresses *what* it wants a holder to present. Every authorization request carries its credential query **inline**: you build a **DCQL query** or a **Presentation Exchange v2 definition** in your application code and send it in the request body when you create the authorization request. Queries live in your codebase, so there are no server-side query records to create, fetch, or delete.

***

## Overview

* A verifier asks for credentials by creating an **authorization request** (an OpenID4VP request). The query that describes the acceptable credentials and fields lives **inside that request body**.
* You supply the query in one of two ways:
  * **`dcqlQuery`** — a Digital Credentials Query Language (DCQL) query, or
  * **`presentationDefinition`** — a DIF Presentation Exchange v2 definition.
* Supply exactly one of the two. If neither is supplied, the request fails with `400 Bad Request` and the message `Either dcqlQuery or presentationDefinition must be provided.` Sending both passes the service's own input validation but fails downstream for every `version` value, surfacing as a `500` (see [Query Language](/develop/verifier/query-language)).
* Because queries are inline, you standardize them in your own application code and pass the built object on each request, rather than registering reusable query records on the server.

***

## Authorization Request Endpoints

These endpoints live under the **`/verifiers/{verifierId}/authorization-requests`** path. The `verifierId` is the identifier of a registered verifier; it may be a DID. Every endpoint requires a valid **OIDC JWT Bearer token** (`Authorization: Bearer <token>`; see [Authentication](/develop/verifier/authentication)).

### Create an Authorization Request

**`POST /verifiers/{verifierId}/authorization-requests`**

Creates an OpenID4VP authorization request and a verification session.

#### Request Body

* **`dcqlQuery`** (object, optional) A DCQL query describing the requested credential configurations. **Required if `presentationDefinition` is not provided.**
* **`presentationDefinition`** (object, optional) A DIF Presentation Exchange v2 definition. **Required if `dcqlQuery` is not provided.** Can only be used with `version` `v1.draft21` or `v1.draft24`.
* **`responseMode`** (string, optional) How the holder returns the presentation. One of `direct_post`, `direct_post.jwt`, `dc_api`, `dc_api.jwt`. Defaults to `direct_post.jwt`.
* **`version`** (string, optional) The OpenID4VP draft to use. One of `v1`, `v1.draft21`, `v1.draft24`. Defaults to `v1`, which supports only `dcqlQuery` — use `v1.draft21` or `v1.draft24` with `presentationDefinition`. Note that `dcqlQuery` cannot be combined with `v1.draft21`.
* **`signingDid`** (string, optional) The DID that signs the authorization request. If omitted, the `verifierId` path parameter must itself be a DID; otherwise the request fails with `400 Bad Request`.
* **`authorizationResponseRedirectUri`** (string, optional) Redirect URI to include in the authorization response.
* **`expectedOrigins`** (array of strings, optional) Expected origins for Digital Credentials API flows (used with the `dc_api` response modes).
* **`transactionData`** (array of objects, optional) Transaction data entries to bind into the request.
* **`verifierInfo`** (array of objects, optional) Verifier attestations to include in the request.

#### Response Body

A success response returns the request details:

* **`authorizationRequestUri`** — an `openid4vp://...` URI the wallet opens to fulfill the request.
* **`verificationSessionId`** — the session identifier used to track and observe the verification.
* **`authorizationRequestId`** — present for hosted requests.
* **`expiresAt`** — ISO timestamp when the request expires.

**Example request body (DCQL)**

```json
{
  "dcqlQuery": {
    "credentials": [
      {
        "id": "employee-badge",
        "format": "dc+sd-jwt",
        "meta": { "vct_values": ["https://issuer.example.com/vct/employee-badge"] },
        "claims": [
          { "path": ["employee_id"] },
          { "path": ["full_name"] },
          { "path": ["department"] }
        ]
      }
    ]
  },
  "responseMode": "direct_post.jwt",
  "version": "v1"
}
```

**Example request body (Presentation Exchange v2)**

```json
{
  "presentationDefinition": {
    "id": "employee-badge-check",
    "input_descriptors": [
      {
        "id": "employee-badge",
        "constraints": {
          "fields": [
            {
              "path": ["$.vct"],
              "filter": { "type": "string", "const": "https://issuer.example.com/vct/employee-badge" }
            },
            { "path": ["$.department"] }
          ]
        }
      }
    ]
  },
  "version": "v1.draft24"
}
```

The `version` field is required here: `presentationDefinition` works only with `v1.draft21` or `v1.draft24`.

**Example response**

```json
{
  "authorizationRequestUri": "openid4vp://?request_uri=...",
  "verificationSessionId": "18b760b1-4ab8-4d38-9d42-6b4c25a4d2e0",
  "authorizationRequestId": "1b8a6a8b-2ac1-4f0b-9d9c-6b9c859b1af2",
  "expiresAt": "2026-06-22T12:34:56.789Z"
}
```

### Create an Authorization Request as a QR Code

**`POST /verifiers/{verifierId}/authorization-requests/qr-code`**

Takes the **same request body** as above, but returns a **PNG image** of a QR code encoding the `authorizationRequestUri`. This is convenient for cross-device flows where a holder scans the code with a wallet.

The request URI and session identifiers are returned in response headers:

* **`X-Authorization-Request-Uri`** — the `openid4vp://...` URI.
* **`X-Verification-Session-Id`** — the verification session identifier.
* **`X-Authorization-Request-Id`** — present for hosted requests.

***

## Observing the Result

After a request is created, track its progress through the verification session endpoints:

* **`GET /verification-sessions/{sessionId}`** — read the current session state (for example `RequestCreated`, and later states as the holder responds).
* **`GET /verification-sessions/{sessionId}/events`** — a **Server-Sent Events (SSE)** stream that emits a `session` event whenever the session updates, plus a `ping` heartbeat every 15 seconds.

Both endpoints require a valid Bearer token.

***

## DCQL vs Presentation Exchange

Both query languages let you target specific credential types and constrain individual claims; pick whichever fits your stack.

* **DCQL** is the newer, compact query model. It describes requested credentials by format and metadata (such as the SD-JWT `vct`) and lists the claim paths you need.
* **Presentation Exchange v2** uses input descriptors with JSONPath `path` expressions and JSON Schema (Draft 7) `filter` rules to validate the value at each path. See the [Query Language](/develop/verifier/query-language) page for more on paths and filters.

The Verifier accepts either form and uses it to match credentials the holder presents. In a DCQL query, request an SD-JWT VC with the `dc+sd-jwt` format value (the [DIF Claim Format Registry](https://identity.foundation/claim-format-registry/) identifier the Verifier uses for SD-JWT credentials).

***

## Notes

* Authentication is an OIDC JWT Bearer token; these endpoints require a valid token but no specific role (see [Authentication](/develop/verifier/authentication)).
* Queries are not persisted as reusable, ID-addressable configurations. Keep your query objects in your own codebase and pass them inline on each request so you can standardize them across flows.


# Security Considerations

The Verifier's security model covers transport, authentication, request binding, and credential integrity:

1. **Transport Security**:\
   All communication occurs over HTTPS, safeguarding data in transit. OpenID4VP requires HTTPS in production; plain HTTP is only permitted in development when explicitly enabled.
2. **Authentication & Authorization**:\
   Verifier endpoints require a valid OIDC bearer token, sent as `Authorization: Bearer <token>` and validated against your identity provider's JWKS endpoint (`OIDC_JWKS_URL`). Authenticated principals are derived from the token's claims, and administrative routes additionally enforce the `admin` role from `realm_access.roles` (client roles in `resource_access.<client>.roles` are also accepted). Store and transmit tokens securely; never expose them client-side. A small set of endpoints is public by design: the health and version routes (`GET /`, `GET /version`), hosted `did:web` documents (`GET /:uuid/did.json`, also served at `/:uuid/.well-known/did.json`), the Swagger docs (`GET /api-docs`, `GET /api-docs-json`), and the wallet-facing OpenID4VP protocol endpoints mounted by the underlying Credo agent (public by protocol design).
3. **Nonce and State Binding**:\
   Authorization requests and presentation responses are bound together by unique, time-bound values defined by the OpenID4VP protocol. This binding prevents replay attacks and ensures a presentation can only be accepted for the request it was issued against.
4. **Signed Authorization Requests**:\
   Each authorization request is signed with the verifier's DID, so wallets can authenticate the relying party before disclosing any data. The signing DID is taken from the `verifierId` when it is itself a DID, or from the optional `signingDid` field in the request body.
5. **Credential Integrity**:\
   The Verifier validates credential signatures and resolves the issuer's DID document to confirm authenticity, rejecting credentials that fail authenticity or integrity checks. SD-JWT selective disclosure is honored so only the claims the holder chooses to reveal are processed.
6. **Minimal Data Exposure**:\
   The Verifier only processes the claims required by your authorization request (expressed via DCQL or a Presentation Exchange definition). It does not request or retain unnecessary information, preserving holder privacy.
7. **Rate Limiting**:\
   The service applies a global request rate limit (60 requests per 60 seconds) to mitigate abuse and brute-force attempts. Account for this limit when designing high-volume verification flows and retry logic.
8. **Monitoring and Logging**:\
   Maintain logs and monitor for suspicious activity, such as repeated failed verifications or token validation errors.
9. **Error Handling**:\
   Avoid exposing sensitive details in error responses. The service returns a consistent error shape (`statusCode`, `message`, `path`, `timestamp`); surface generic user-facing messages and keep technical details in your internal logs.

For local development without tokens, see the development bypass in [Authentication](/develop/verifier/authentication#development-mode).


# Error Handling and Troubleshooting

The Verifier returns clear HTTP status codes and a consistent JSON error shape when something goes wrong. A global exception filter wraps every failure in the same envelope, so your integration can parse errors uniformly regardless of which endpoint produced them.

## Common Error Codes

* **400 Bad Request**: Invalid input or a malformed request body. The most common cause when creating an authorization request is supplying neither query form — at least one of `dcqlQuery` or `presentationDefinition` is required. Other causes include a `dcqlQuery` or `presentationDefinition` value that is not a JSON object, or omitting `signingDid` when the `verifierId` path parameter is not itself a DID. The global validation pipe also rejects request bodies containing unknown JSON properties (`property X should not exist`).
* **401 Unauthorized**: Missing, malformed, or expired OIDC bearer token. Every non-public endpoint requires `Authorization: Bearer <token>`, validated against your identity provider's JWKS endpoint (`OIDC_JWKS_URL`). A token is rejected here if its signature does not verify against the JWKS keys, if it is expired, or if it is missing or malformed (for example, a token whose header has no `kid`).
* **403 Forbidden**: The token is valid but the principal lacks the required role. Administrative routes (DID, issuer, verifier, and holder management under `/agent/*` and `/holders/*`) additionally enforce the `admin` role from `realm_access.roles` (client roles in `resource_access.<client>.roles` are also accepted); a non-admin token receives `Insufficient role`. The core Verifier endpoints (authorization requests, verification sessions, and `/credentials/verify`) require a valid token but no specific role.
* **404 Not Found**: A mistyped or unmatched route path (the default response for an unknown route), or a request for a hosted `did:web` document that the service is not serving (the hosted-document routes return `DID Document not found`).
* **500 Internal Server Error**: An unexpected condition, configuration error, or a failed downstream operation such as DID resolution. A structurally invalid DCQL query or Presentation Exchange definition (a JSON object that does not conform to the query language's schema) also surfaces as a `500`, because the structure is validated downstream by the underlying OpenID4VP library rather than by the service's own input validation. This also covers an unknown verification session id or an unresolvable `verifierId`: these surface as a `500` rather than a `404`, because the underlying record-not-found errors are not mapped to a not-found status. In production the response message is generic; consult your server logs for the underlying cause.

## Example Error Response

The service returns the same envelope for every error: `statusCode`, `message`, `path`, and `timestamp`. For `HttpException`-based failures the `message` may also be an array of validation strings.

```json
{
  "statusCode": 400,
  "message": "Either dcqlQuery or presentationDefinition must be provided.",
  "path": "/verifiers/did:web:verifier.example.com:9b2c1d3e-4f5a-6789-abcd-ef0123456789/authorization-requests",
  "timestamp": "2026-06-22T10:15:30.123Z"
}
```

There is no `code` field. Parse `statusCode` for the HTTP status and `message` for the human-readable reason.

## Troubleshooting Tips

* **Validate your credential query.** Ensure your request includes exactly one of `dcqlQuery` or `presentationDefinition`, and that the chosen form is well-formed. A `400` with the message `Either dcqlQuery or presentationDefinition must be provided.` means both were absent. A structurally invalid query that is still a JSON object fails downstream and returns a `500` instead — validate your DCQL or Presentation Exchange definition against its specification before sending.
* **Check your bearer token.** Confirm the `Authorization: Bearer <token>` header is present, the token is unexpired, and it was issued by the provider behind `OIDC_JWKS_URL`. A `401` points to the token itself; a `403` with `Insufficient role` means the token is valid but lacks the `admin` role required by administrative routes.
* **Verify session, nonce, and state.** When a presentation is rejected, confirm the wallet is responding to the correct authorization request — presentations are bound to their originating request by nonce and state, and a mismatch will fail verification. Note that requesting an unknown or expired verification session id does not return a `404`; it currently surfaces as a `500` with a generic message in production, so check your server logs to distinguish a missing session from another fault.
* **Confirm DID resolution.** Verification relies on resolving the issuer's DID document. For `did:web` issuers, the document must be reachable over HTTPS at the URL derived from the DID (for `did:web:<domain>:<uuid>`, `https://<domain>/<uuid>/did.json`; bare-domain DIDs use `/.well-known/did.json`); an unreachable host or a `did.json` that is not served will cause credential verification to fail. `did:key` and `did:jwk` resolve from the identifier itself.
* **Check transport security.** OpenID4VP requires HTTPS in production. Plain HTTP is only permitted in development when explicitly enabled via `ALLOW_INSECURE_HTTP`.
* **Mind the rate limit.** The service applies a global limit of 60 requests per 60 seconds. High-volume flows that exceed it receive `429 Too Many Requests`; add backoff and retry logic accordingly.
* **Review server logs.** `500` responses return a generic message in production; the detailed stack trace and request path are recorded in the service logs.

To rule out token problems while debugging locally, use the development bypass described in [Authentication](/develop/verifier/authentication#development-mode).


# Future Enhancements

The Verifier already covers the core of an OpenID4VP presentation flow today: it creates DID-signed authorization requests, describes the credentials you need with **DCQL** or **DIF Presentation Exchange v2**, verifies **SD-JWT VC** and **JWT-VC** presentations, honors **SD-JWT selective disclosure**, and streams session state over **SSE**. The items below are on the roadmap to extend that surface. They are **not implemented yet** — they are listed here so you can plan an integration around what exists now rather than what is planned.

## Roadmap

* **Enhanced selective disclosure and predicate proofs**: SD-JWT selective disclosure — revealing only the claims a verifier requests — is supported today. The roadmap extends this toward **predicate proofs**, where a holder can prove a statement about a claim (for example, "is over 18" or "salary band is X") without revealing the underlying value, further minimizing data exposure.
* **Zero-Knowledge Proofs (ZKP)**: Building on predicate proofs, ZKP support would let holders prove specific facts derived from a credential without disclosing the credential's contents at all, improving privacy and regulatory compliance for high-sensitivity use cases.
* **Additional client libraries**: Integration today is plain HTTP plus an SSE subscription, so any language works. Planned helper libraries beyond JavaScript/TypeScript would wrap request creation, QR/deep-link rendering, and session streaming to shorten time-to-integration in more ecosystems.
* **Broader DID-method resolution**: The Verifier resolves `did:web`, `did:key`, and `did:jwk` today. As the wider Empeiria ecosystem matures, support for resolving additional DID methods — including the **EMPE DID method (`did:empe`)** anchored on the EMPE blockchain — is an ecosystem direction. This is **not yet available** in the Verifier.


# References and Standards

The references below cover the protocols, query formats, credential formats, and identifier methods the Verifier implements.

## Protocols

* **OpenID for Verifiable Presentations (OpenID4VP)**: The protocol the Verifier uses to request, receive, and validate Verifiable Presentations. The Verifier supports the `v1`, `v1.draft21`, and `v1.draft24` versions, selectable per authorization request. <https://openid.net/specs/openid-4-verifiable-presentations-1_0.html>

## Query Formats

* **DCQL (Digital Credentials Query Language)**: A compact, credential-centric query format for describing the credentials and claims a presentation must contain. Defined as part of the OpenID4VP specification. <https://openid.net/specs/openid-4-verifiable-presentations-1_0.html>
* **DIF Presentation Exchange v2**: A standard, vendor-neutral way to express credential requirements using input descriptors, JSONPath `path` expressions, and JSON Schema `filter` constraints. Each authorization request carries exactly one of a `dcqlQuery` or a `presentationDefinition`. <https://identity.foundation/presentation-exchange/spec/v2.0.0/>

## Credential Formats

* **W3C Verifiable Credentials Data Model**: The foundational data model for Verifiable Credentials and Verifiable Presentations. <https://www.w3.org/TR/vc-data-model/>
* **SD-JWT VC (Selective Disclosure JWT Verifiable Credentials)**: A JWT-based credential format that supports **selective disclosure**, so a holder can reveal only the claims a verifier requests (`dc+sd-jwt`). <https://datatracker.ietf.org/doc/draft-ietf-oauth-sd-jwt-vc/>
* **JWT-VC-JSON**: A JWT-based W3C Verifiable Credential (`jwt_vc_json`), following the W3C VC Data Model. <https://www.w3.org/TR/vc-data-model/>

## Identifiers

* **Decentralized Identifier (DID) Core**: The core specification defining DIDs and DID Documents, which carry the public keys and service endpoints used to establish trust. <https://www.w3.org/TR/did-core/>
* **`did:web` Method**: A DID method whose DID Document is resolved over HTTPS at a domain. A verifier can sign its authorization requests with a `did:web` identifier. <https://w3c-ccg.github.io/did-method-web/>
* **`did:key` Method**: A self-contained DID method in which the public key is encoded directly in the identifier, requiring no external resolution. <https://w3c-ccg.github.io/did-method-key/>
* **`did:jwk` Method**: A self-contained DID method in which a JSON Web Key (JWK) is encoded directly in the identifier, so it resolves without any external lookup. <https://github.com/quartzjer/did-jwk/blob/main/spec.md>


# MCP Server Integration

The [Empe MCP Server](https://github.com/empe-io/mcp-server) is an **external companion project**, maintained in its own repository, that lets AI systems like Claude drive verification flows through natural language. It is a Model Context Protocol (MCP) server that exposes verification operations as tools an AI client can call; it is not part of the Verifier Service deployment itself.

## What is MCP?

MCP (Model Context Protocol) servers provide context, tools, and prompts to AI clients, making APIs and data sources accessible to AI systems. The Empe MCP server focuses on Self-Sovereign Identity operations, including verification workflows.

## Available Verifier Tools

The MCP server registers these verifier-related tools:

### Verification Workflows

* `generate_verification_qr` - Create QR codes for credential verification
* `generate_verification_qr_for_vp_query` - Generate QR codes for a specific VP query
* `check_verification_status` - Monitor verification status

### VP Query Management

* `create_vp_query` - Create new Verifiable Presentation queries
* `get_all_vp_queries` - List the VP queries the MCP server manages
* `get_vp_query_by_id` - Retrieve a specific VP query
* `delete_vp_query` - Remove unused VP queries

The MCP server manages its own verification workflow and VP-query records against [One-Click](https://oneclick.empe.io/) deployments; its tools do not map one-to-one onto the Verifier API endpoints documented in this section. See the [repository README](https://github.com/empe-io/mcp-server) for how each tool works.

## Example Prompts

```
"Generate a QR code to verify event tickets for the Annual Developer Conference."

"Create a verification that checks if someone has a MembershipCard credential
with 'Gold' level that hasn't expired yet."

"Check if anyone has scanned the verification QR code I generated earlier."

"Show me all the verification queries we have in the system."
```

## Setup

1. **Deploy a Verifier Service** via [One-Click Deployment](https://oneclick.empe.io/)
2. **Configure external access** (the verifier must be reachable from outside your network)
3. **Set up the MCP server** with your verifier's endpoints and credentials
4. **Connect the MCP server** to your AI client (like Claude)

The MCP server's verifier-related environment variables:

```
VERIFIER_BASE_URL=https://your-verifier-instance.example.com
VERIFIER_CLIENT_URL=https://your-external-accessible-url.com
CLIENT_SECRET=your-verifier-client-secret
SSE_PORT=8080
```

For full setup instructions and compatibility notes, see the [MCP Server repository](https://github.com/empe-io/mcp-server).

## Security Considerations

When using the MCP server with production verifier services:

* Use dedicated test environments for experimentation
* Validate all AI-generated VP queries before production deployment
* Monitor verification requests and results
* Implement proper access controls for the MCP server
* Ensure external verifier URLs use HTTPS and proper authentication


# FAQ

**Q: How do I integrate the Verifier into my existing authentication flow?**

A: You integrate by calling the Verifier's HTTP API directly from your backend — no client SDK is required. Your server creates an authorization request with **`POST /verifiers/:verifierId/authorization-requests`** (or its `/qr-code` variant) using a valid `Authorization: Bearer <token>` header, renders the returned `authorizationRequestUri` as a QR code or deep link for the wallet, and then subscribes to **`GET /verification-sessions/:sessionId/events`** over Server-Sent Events to react in real time. When the session reaches a verified state and the claims you required are present, you can complete your own login or access-control decision. This complements or replaces traditional authentication methods such as passwords.

**Q: Can the Verifier handle multiple credential types?**

A: Yes. There is no fixed set of flows to configure — you describe exactly which credentials and claims you need per request by supplying either a **DCQL** query or a **DIF Presentation Exchange v2** definition in the authorization request body. Provide exactly one of `dcqlQuery` or `presentationDefinition`; sending a different query lets the same Verifier accept a different credential type (for example an `EmployeeBadge@1:sd-jwt` credential in one flow and an employment credential in another). See [Query Language](/develop/verifier/query-language) for examples.

**Q: How do I know if a credential issuer is trustworthy?**

A: Issuer authenticity is established cryptographically. When a presentation arrives, the Verifier resolves the issuer's DID — a `did:web` document fetched over HTTPS, or a `did:key` resolved from the identifier itself — and verifies the credential signature against the key in that DID document. Credentials that fail authenticity or integrity checks are rejected. Trust today rests on DID resolution and signature verification; revocation-status and domain-linkage checks are planned additions (see [Future Enhancements](/develop/verifier/future-enhancements)).

**Q: What if a verification request expires or fails?**

A: Each authorization request creates a **verification session** that carries an expiry (`expiresAt`) and advances through lifecycle states (for example `RequestCreated`) that you can read from **`GET /verification-sessions/:sessionId`** or stream over SSE. If a session expires before the wallet responds, simply create a new authorization request and present the fresh URI or QR code. When verification fails, the session exposes an error and `/credentials/verify` returns `{ "isValid": false, "error": "..." }`; check the message together with your query and the credential definition to diagnose the cause.

**Q: Can I customize the verification workflow?**

A: Yes. Customization happens in two places. First, shape what you ask for by tailoring your `dcqlQuery` or `presentationDefinition` — credential types, claims, JSON Schema filters, the OpenID4VP `version`, and the `responseMode`. Second, apply your own business logic in your backend by reading the verification session state (via polling or the SSE stream) and deciding what a successfully verified presentation means for your application. There is no separate callback-handler framework; you own the handling of the verified session in your own code.


# API Reference


# Authorization Requests

An **authorization request** is how the Verifier asks a holder to present credentials. Each request is an **OpenID4VP** request that carries its credential query inline and, once created, is tracked by a **verification session**. This page documents the two endpoints that create authorization requests: one that returns request details as JSON, and one that returns the same request encoded as a scannable **QR code**.

You build the query in your own application code and send it on every call, choosing between a **DCQL query** and a **DIF Presentation Exchange v2 definition**; the server does not store reusable request templates.

***

## Endpoints

Both endpoints live under the **`/verifiers/{verifierId}/authorization-requests`** path. The `verifierId` is the identifier of a registered verifier; it may be a DID. Every endpoint requires a valid **OIDC JWT Bearer token** (`Authorization: Bearer <token>`); no specific role is required (see [Authentication](/develop/verifier/authentication)).

***

## Create an Authorization Request

**`POST /verifiers/{verifierId}/authorization-requests`**

Creates an OpenID4VP authorization request and a new verification session, then returns the request URI and session identifiers as JSON.

### Request Body

Every field is optional, but **at least one** of `dcqlQuery` or `presentationDefinition` must be supplied. If neither is present, the request fails with `400 Bad Request` and the message `Either dcqlQuery or presentationDefinition must be provided.` Supply exactly one — sending both passes the service's own input validation but fails downstream for every `version` value, surfacing as a `500`.

* **`dcqlQuery`** (object) A Digital Credentials Query Language (DCQL) query describing the requested credentials and claim paths. **Required if `presentationDefinition` is not provided.**
* **`presentationDefinition`** (object) A DIF Presentation Exchange v2 definition. **Required if `dcqlQuery` is not provided.** Can only be used with `version` `v1.draft21` or `v1.draft24` — a request with `presentationDefinition` and no explicit `version` fails with an internal error (`OpenID4VP version 'v1' cannot be used with presentationExchange`).
* **`responseMode`** (string) How the holder returns the presentation. One of `direct_post`, `direct_post.jwt`, `dc_api`, `dc_api.jwt`. Defaults to **`direct_post.jwt`**.
* **`version`** (string) The OpenID4VP draft to use. One of `v1`, `v1.draft21`, `v1.draft24`. Defaults to **`v1`**, which supports only `dcqlQuery`; use `v1.draft21` or `v1.draft24` with `presentationDefinition`. `dcqlQuery` cannot be combined with `v1.draft21`.
* **`signingDid`** (string) The DID that signs the authorization request. If omitted, the `verifierId` path parameter must itself be a DID; otherwise the request fails with `400 Bad Request` and the message ``Missing signing DID. Provide a `signingDid` in the request body when the verifierId is not a DID.``
* **`authorizationResponseRedirectUri`** (string) Redirect URI to include in the authorization response.
* **`expectedOrigins`** (array of strings) Expected origins for Digital Credentials API flows (used with the `dc_api` and `dc_api.jwt` response modes).
* **`transactionData`** (array of objects) Transaction data entries to bind into the request.
* **`verifierInfo`** (array of objects) Verifier attestations to include in the request.

### Response Body

A `201 Created` response returns the request details:

* **`authorizationRequestUri`** — an `openid4vp://...` URI the wallet opens to fulfill the request.
* **`verificationSessionId`** — the session identifier used to track and observe the verification.
* **`authorizationRequestId`** — present for hosted requests.
* **`expiresAt`** — ISO timestamp when the request expires.

**Example request body (DCQL, SD-JWT VC)**

```json
{
  "responseMode": "direct_post.jwt",
  "version": "v1",
  "dcqlQuery": {
    "credentials": [
      {
        "id": "employee_badge",
        "format": "dc+sd-jwt",
        "meta": {
          "vct_values": ["https://issuer.example.com/vct/employee-badge"]
        },
        "claims": [
          { "path": ["employee_id"] },
          { "path": ["full_name"] },
          { "path": ["department"] }
        ]
      }
    ]
  }
}
```

**Example request body (Presentation Exchange v2)**

```json
{
  "responseMode": "direct_post.jwt",
  "version": "v1.draft24",
  "presentationDefinition": {
    "id": "employee-badge-check",
    "input_descriptors": [
      {
        "id": "employee_badge",
        "constraints": {
          "fields": [
            {
              "path": ["$.vct"],
              "filter": {
                "type": "string",
                "const": "https://issuer.example.com/vct/employee-badge"
              }
            },
            { "path": ["$.department"] }
          ]
        }
      }
    ]
  }
}
```

**Example response**

```json
{
  "authorizationRequestUri": "openid4vp://?request_uri=...",
  "verificationSessionId": "18b760b1-4ab8-4d38-9d42-6b4c25a4d2e0",
  "authorizationRequestId": "1b8a6a8b-2ac1-4f0b-9d9c-6b9c859b1af2",
  "expiresAt": "2026-06-22T12:34:56.789Z"
}
```

***

## Create an Authorization Request as a QR Code

**`POST /verifiers/{verifierId}/authorization-requests/qr-code`**

Takes the **same request body** as the JSON endpoint above, but instead of JSON it returns a **PNG image** of a QR code encoding the `authorizationRequestUri`. This is convenient for cross-device flows where a holder scans the code with a wallet.

### Request Body

Identical to [Create an Authorization Request](#create-an-authorization-request). The same validation rules apply, including the requirement that at least one of `dcqlQuery` or `presentationDefinition` be present.

### Response Body

The response body is the raw QR code as `image/png` (`Content-Type: image/png`). The request URI and session identifiers are returned in response headers instead of a JSON body:

* **`X-Authorization-Request-Uri`** — the `openid4vp://...` URI encoded in the QR code.
* **`X-Verification-Session-Id`** — the verification session identifier.
* **`X-Authorization-Request-Id`** — present for hosted requests.

These headers are listed in `Access-Control-Expose-Headers`, so browser-based clients can read them from a cross-origin response.

**Example request**

```bash
curl -X POST \
  "https://verifier.example.com/verifiers/did:web:verifier.example.com/authorization-requests/qr-code" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "version": "v1",
    "dcqlQuery": {
      "credentials": [
        {
          "id": "employee_badge",
          "format": "dc+sd-jwt",
          "meta": { "vct_values": ["https://issuer.example.com/vct/employee-badge"] },
          "claims": [{ "path": ["employee_id"] }, { "path": ["department"] }]
        }
      ]
    }
  }' \
  --output authorization-request.png --dump-header -
```

The downloaded `authorization-request.png` is the QR code, while the dumped headers contain `X-Authorization-Request-Uri`, `X-Verification-Session-Id`, and (for hosted requests) `X-Authorization-Request-Id`.

***

## Observing the Result

Both endpoints create a verification session as a side effect. Track its progress through the verification session endpoints:

* **`GET /verification-sessions/{sessionId}`** — read the current session state.
* **`GET /verification-sessions/{sessionId}/events`** — a **Server-Sent Events (SSE)** stream that emits a `session` event whenever the session updates, plus a `ping` heartbeat every 15 seconds.

Both endpoints require a valid Bearer token. See [Defining What Credentials to Request](/develop/verifier/server-side-vp-query) for guidance on building DCQL and Presentation Exchange queries.

***

## Notes

* Pass `signingDid` whenever the `verifierId` is not itself a DID; the authorization request must be signed by a DID the service controls.
* Authentication is an OIDC JWT Bearer token; the authorization-request endpoints require a valid token but no specific role.


# Verification Sessions

A **verification session** tracks a single OpenID4VP exchange from the moment you create an authorization request until the holder's presentation is verified (or the session errors out). When you create an authorization request, the Verifier returns a `verificationSessionId`; you use that identifier to read the session's current state, or to subscribe to live updates over a **Server-Sent Events (SSE)** stream.

This page documents the two session endpoints: reading a session once with `GET /verification-sessions/{sessionId}`, and watching it in real time with `GET /verification-sessions/{sessionId}/events`.

***

## Overview

* A session is created for you by the authorization-request endpoints (see [Defining What Credentials to Request](/develop/verifier/server-side-vp-query)). You do not create sessions directly.
* Every session belongs to a registered verifier and carries a **state** that advances as the holder interacts with the request.
* You can observe a session two ways:
  * **Poll** the session with `GET /verification-sessions/{sessionId}` whenever you need its latest state.
  * **Subscribe** to `GET /verification-sessions/{sessionId}/events` and react to `session` events as the Verifier pushes them, which avoids polling.
* Both endpoints require a valid **OIDC JWT Bearer token** (`Authorization: Bearer <token>`). They carry no specific role requirement (see [Authentication](/develop/verifier/authentication)).

***

## Session States

A session moves through the following states. Read the current value from the session's **`state`** field.

* **`RequestCreated`** — the authorization request has been created and is waiting for a wallet to pick it up. This is the initial state.
* **`RequestUriRetrieved`** — a wallet has fetched the request URI and is in the process of responding.
* **`ResponseVerified`** — the holder returned a presentation and the Verifier successfully verified it. This is the terminal success state.
* **`Error`** — the session failed (for example, an invalid presentation was supplied). The **`errorMessage`** field describes what went wrong. This is the terminal failure state.

Treat `ResponseVerified` and `Error` as final outcomes; once a session reaches either, no further state changes occur.

***

## Get a Verification Session

**`GET /verification-sessions/{sessionId}`**

Returns the verification session identified by `sessionId`. Use this to read the session's current state on demand.

### Path Parameters

* **`sessionId`** (string, required) — the verification session identifier returned when the authorization request was created.

### Response Body

A `200 OK` response returns the session record:

* **`id`** — the session identifier.
* **`verifierId`** — the identifier of the verifier that created the session (may be a DID).
* **`state`** — the current session state (see [Session States](#session-states)).
* **`authorizationRequestId`** — the authorization request identifier, present for hosted requests.
* **`authorizationRequestUri`** — the `openid4vp://...` URI the wallet opens to fulfill the request.
* **`authorizationResponseRedirectUri`** — the redirect URI supplied when the request was created, if any.
* **`expiresAt`** — ISO timestamp when the request expires, if set.
* **`errorMessage`** — the last error observed in the session lifecycle, present when `state` is `Error`.

**Example response**

```json
{
  "id": "18b760b1-4ab8-4d38-9d42-6b4c25a4d2e0",
  "verifierId": "verifier-empe",
  "state": "RequestCreated",
  "authorizationRequestId": "1b8a6a8b-2ac1-4f0b-9d9c-6b9c859b1af2",
  "authorizationRequestUri": "openid4vp://?request_uri=...",
  "authorizationResponseRedirectUri": "https://verifier.example.com/callback",
  "expiresAt": "2026-06-22T12:34:56.789Z"
}
```

Once the holder has presented an `EmployeeBadge@1.0:sd-jwt` credential and the Verifier has checked it, the same call returns `"state": "ResponseVerified"`.

***

## Watch a Verification Session (SSE)

**`GET /verification-sessions/{sessionId}/events`**

Opens a **Server-Sent Events (SSE)** stream for the session. The Verifier immediately emits the session's current state, then pushes a new event every time the state changes — so you can drive UI updates without polling.

### Path Parameters

* **`sessionId`** (string, required) — the verification session identifier.

### Event Types

The stream emits two kinds of events, distinguished by the SSE `event:` field:

* **`session`** — carries the full session record as its `data` payload. The same fields as the [`GET` response](#response-body) are included, serialized as JSON. The first `session` event reflects the session's current state at subscription time; each subsequent one corresponds to a state change.
* **`ping`** — a heartbeat with the `data` payload `keepalive`, emitted every **15 seconds**. It carries no session data; its only purpose is to keep the connection alive and let you detect a dropped stream. Ignore `ping` events in your handler.

**Example stream**

```
event: session
data: {"id":"18b760b1-4ab8-4d38-9d42-6b4c25a4d2e0","verifierId":"verifier-empe","state":"RequestCreated","authorizationRequestId":"1b8a6a8b-2ac1-4f0b-9d9c-6b9c859b1af2","authorizationRequestUri":"openid4vp://?request_uri=...","expiresAt":"2026-06-22T12:34:56.789Z"}

event: ping
data: keepalive

event: session
data: {"id":"18b760b1-4ab8-4d38-9d42-6b4c25a4d2e0","verifierId":"verifier-empe","state":"RequestUriRetrieved","authorizationRequestId":"1b8a6a8b-2ac1-4f0b-9d9c-6b9c859b1af2","authorizationRequestUri":"openid4vp://?request_uri=...","expiresAt":"2026-06-22T12:34:56.789Z"}

event: session
data: {"id":"18b760b1-4ab8-4d38-9d42-6b4c25a4d2e0","verifierId":"verifier-empe","state":"ResponseVerified","authorizationRequestId":"1b8a6a8b-2ac1-4f0b-9d9c-6b9c859b1af2","authorizationRequestUri":"openid4vp://?request_uri=...","expiresAt":"2026-06-22T12:34:56.789Z"}
```

The stream stays open after a terminal state; close it from the client once you observe `ResponseVerified` or `Error`.

### Consuming the Stream with `EventSource`

In the browser, use the native [`EventSource`](https://developer.mozilla.org/en-US/docs/Web/API/EventSource) API. Because `session` and `ping` are **named** events, register a listener for each with `addEventListener`; the default `onmessage` handler only fires for unnamed events and will not receive them.

```javascript
const sessionId = "18b760b1-4ab8-4d38-9d42-6b4c25a4d2e0";
const source = new EventSource(
  `https://verifier.example.com/verification-sessions/${sessionId}/events`
);

source.addEventListener("session", (event) => {
  const session = JSON.parse(event.data);
  console.log("Session state:", session.state);

  if (session.state === "ResponseVerified") {
    console.log("Holder verified — grant access.");
    source.close();
  } else if (session.state === "Error") {
    console.error("Verification failed:", session.errorMessage);
    source.close();
  }
});

source.addEventListener("ping", () => {
  // Heartbeat — connection is alive; nothing to do.
});

source.onerror = (error) => {
  // The connection dropped; EventSource reconnects automatically.
  console.warn("SSE connection error:", error);
};
```

The browser's `EventSource` does not let you set custom request headers, so it cannot attach an `Authorization: Bearer` token directly. Because this endpoint requires a valid token, the browser must not connect to it directly in production. Instead, terminate the SSE stream behind your own backend: hold the Bearer token there, subscribe to the Verifier's stream with a server-side SSE client (or HTTP client that streams the response), and relay the `session` and `ping` events to the browser over a route on your own origin. See [Front-End Integration](/develop/verifier/frontend-integration) for a full backend-relay example. Only in local development with `AUTH_DISABLED=true` can the browser connect to the endpoint directly.

***

## Notes

* Sessions are created by the authorization-request endpoints; this page covers reading and watching them. See [Defining What Credentials to Request](/develop/verifier/server-side-vp-query) for how a session begins.
* Authentication is an OIDC JWT Bearer token; these endpoints require a valid token but no specific role.
* The SSE stream is the recommended way to react to verification outcomes in near real time; fall back to polling `GET /verification-sessions/{sessionId}` where a long-lived connection is impractical.


# Verify Credential

This page documents the Verifier's **direct credential verification** endpoint. It takes a single credential in its compact serialized form, checks the signature, and returns the decoded payload. This is a stateless, one-shot check of one credential — it is **not** a full presentation flow. If you need a wallet to choose and present credentials interactively (with selective disclosure, holder binding, and session tracking), use the authorization-request and verification-session endpoints described in [Defining What Credentials to Request](/develop/verifier/server-side-vp-query) and [Verifier Client Configuration](/develop/verifier/client-configuration) instead.

Use this endpoint when you already hold a compact credential string — for example one you received out of band, exported from a wallet, or signed yourself with the Issuer's signing endpoint — and you want to confirm it is authentic and read its claims.

***

## Overview

* The endpoint verifies a **single credential** supplied as a compact string. No verification session is created and nothing is persisted.
* Two credential formats are supported: **SD-JWT VC** and **JWT-VC-JSON**. The format is detected automatically from the string itself — there is no `format` field in the request.
* On success you get back the decoded `payload`, and for SD-JWT credentials a resolved `prettyClaims` object with all disclosed claims merged in.
* On failure you get a `false` result and an `error` message rather than an HTTP error, so your code can branch on the result instead of catching exceptions. (A malformed request body — for example a missing or empty `credential` — still returns `400 Bad Request`.)

***

## Endpoint

This endpoint lives under the **`/credentials`** path. It requires a valid **OIDC JWT Bearer token** (`Authorization: Bearer <token>`); it carries no specific role requirement (see [Authentication](/develop/verifier/authentication)).

### Verify a Credential

**`POST /credentials/verify`**

Verifies a credential's signature and returns the decoded payload.

#### Request Body

* **`credential`** (string, required) The credential to verify, in compact serialized form. This is either a compact **SD-JWT** (the issuer-signed JWT followed by `~`-separated disclosures, ending in `~`) or a compact **JWT-VC-JSON** (a three-part `header.payload.signature` JWT). The string must be non-empty.

The format is inferred from the value: a string containing a `~` is treated as an SD-JWT; a string of exactly three non-empty dot-separated segments with no `~` is treated as a JWT-VC. Anything else is rejected (see [Error Responses](#error-responses)).

```json
{
  "credential": "eyJhbGciOiJFZERTQSJ9.eyJ2Y3QiOiJodHRwczovL2lzc3Vlci5leGFtcGxlLmNvbS92Y3QvZW1wbG95ZWUtYmFkZ2UiLCJlbXBsb3llZV9pZCI6IkUtMTAyNCJ9.signature~WyJzYWx0IiwiZnVsbF9uYW1lIiwiQWRhIExvdmVsYWNlIl0~WyJzYWx0IiwiZGVwYXJ0bWVudCIsIkVuZ2luZWVyaW5nIl0~"
}
```

#### Response Body

A `200 OK` response always carries an **`isValid`** boolean. The remaining fields depend on the outcome.

When the credential is valid:

* **`isValid`** (boolean) — `true`.
* **`format`** (string) — the detected credential format. `dc+sd-jwt` for SD-JWT VC, or `jwt_vc` for JWT-VC-JSON.
* **`payload`** (object) — the decoded credential payload (the claims carried in the signed JWT body).
* **`prettyClaims`** (object, SD-JWT only) — the credential's claims with all disclosures resolved and merged. This field is present only for SD-JWT credentials; JWT-VC responses omit it.

When the credential is invalid:

* **`isValid`** (boolean) — `false`.
* **`error`** (string) — a human-readable reason the credential could not be verified.

A failure response contains only `isValid: false` and `error`. It never includes `format` (or `payload`/`prettyClaims`), even when the credential was parseable as an SD-JWT or JWT-VC — the controller discards the detected format on invalid results.

**Example success response (SD-JWT VC)**

For an `EmployeeBadge@1:sd-jwt` credential, the response merges the disclosed claims into `prettyClaims` while `payload` reflects the raw signed body:

```json
{
  "isValid": true,
  "format": "dc+sd-jwt",
  "payload": {
    "vct": "https://issuer.example.com/vct/employee-badge",
    "iss": "did:web:issuer.example.com:1f8a6a8b",
    "iat": 1750593600,
    "employee_id": "E-1024"
  },
  "prettyClaims": {
    "vct": "https://issuer.example.com/vct/employee-badge",
    "employee_id": "E-1024",
    "full_name": "Ada Lovelace",
    "department": "Engineering"
  }
}
```

**Example success response (JWT-VC-JSON)**

```json
{
  "isValid": true,
  "format": "jwt_vc",
  "payload": {
    "iss": "did:web:issuer.example.com:1f8a6a8b",
    "sub": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
    "vc": {
      "type": ["VerifiableCredential", "EmploymentCredential"],
      "credentialSubject": {
        "employee_id": "E-1024",
        "full_name": "Ada Lovelace",
        "department": "Engineering"
      }
    }
  }
}
```

**Example failure response**

```json
{
  "isValid": false,
  "error": "SD-JWT verification failed."
}
```

***

## Example Request

The example below verifies a credential from a backend using TypeScript and `fetch`. The same call works from any HTTP client.

```typescript
const VERIFIER_BASE_URL = 'https://your-verifier.evdi.app';

async function verifyCredential(token: string, credential: string) {
  const res = await fetch(`${VERIFIER_BASE_URL}/credentials/verify`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${token}`,
    },
    body: JSON.stringify({ credential }),
  });

  const result = await res.json();

  if (result.isValid) {
    // Read the credential's claims. For SD-JWT, prefer prettyClaims.
    const claims = result.prettyClaims ?? result.payload;
    console.log('Verified', result.format, claims);
  } else {
    console.warn('Credential is not valid:', result.error);
  }

  return result;
}
```

***

## Error Responses

* **Unsupported format** — if the `credential` string is neither a compact SD-JWT nor a three-part JWT, the response is `200 OK` with `isValid: false` and the message `Unsupported credential format. Expected compact JWT (x.y.z) or SD-JWT.`
* **Verification failure** — if the signature is invalid, the credential is expired, or the issuer's verification material cannot be resolved, the response is `200 OK` with `isValid: false` and an `error` describing the cause.
* **Validation failure** — if the request body is missing the `credential` field or it is empty, the request fails with `400 Bad Request` from the global error filter (`{ statusCode, message, path, timestamp }`).

***

## Notes

* This endpoint verifies **one credential at a time**. It does not evaluate a Verifiable Presentation, check holder binding against a presentation, or apply a DCQL / Presentation Exchange query. For those, create an authorization request and observe the verification session.
* This endpoint requires a valid OIDC JWT Bearer token but no specific role (see [Authentication](/develop/verifier/authentication)).
* For SD-JWT credentials, read disclosed values from `prettyClaims`. The `payload` field contains the raw signed body, in which selectively disclosable claims are represented as hashed digests rather than their plaintext values.


# VP Query

## GET /api/v1/verifier/vp-queries

> Retrieves all VP query configurations.

```json
{"openapi":"3.0.0","info":{"title":"verifier-service","version":"1.0.0-rc1"},"servers":[{"url":"/"}],"security":[{"clientSecret":[]}],"components":{"securitySchemes":{"clientSecret":{"type":"apiKey","name":"x-client-secret","in":"header"}},"schemas":{"VpQueryConfiguration":{"description":"Entity representing a Verifiable Presentation (VP) query configuration.\n\nVP queries define what credentials a verifier is requesting from a user.\nThese configurations can be reused across multiple authorization requests,\nallowing for standardized verification processes.","properties":{"id":{"type":"string","description":"Unique identifier for the VP query configuration.\nAutomatically generated as a UUID."},"queryBody":{"items":{"$ref":"#/components/schemas/VPQueryParams"},"type":"array","description":"The body of the VP query as a JSON array.\nContains the parameters that define what credentials are being requested,\nincluding types, formats, and constraints."},"createdAt":{"type":"number","format":"double","description":"Timestamp when this VP query configuration was created.\nStored as a bigint representing milliseconds since the Unix epoch."}},"required":["id","queryBody"],"type":"object","additionalProperties":false},"VPQueryParams":{"properties":{"fields":{"items":{"$ref":"#/components/schemas/InputDescriptorField"},"type":"array","description":"List of descriptor fields that define paths and optional filters"}},"required":["fields"],"type":"object","description":"Parameters for querying Verifiable Presentations.\nContains fields to specify which parts of a credential to query\nand optional filters to apply."},"InputDescriptorField":{"properties":{"filter":{"$ref":"#/components/schemas/JSONSchema7","description":"Optional JSON Schema that values must conform to"},"path":{"items":{"type":"string"},"type":"array","description":"Array of JSONPath expressions to locate values in the credential"}},"required":["path"],"type":"object","description":"Describes a field within an input descriptor.\nUsed to specify which paths to extract from a credential\nand optional JSON Schema filter to validate the values."},"JSONSchema7":{"properties":{"$id":{"type":"string"},"$ref":{"type":"string"},"$schema":{"$ref":"#/components/schemas/JSONSchema7Version"},"$comment":{"type":"string"},"$defs":{"properties":{},"additionalProperties":{"$ref":"#/components/schemas/JSONSchema7Definition"},"type":"object"},"type":{"anyOf":[{"$ref":"#/components/schemas/JSONSchema7TypeName"},{"items":{"$ref":"#/components/schemas/JSONSchema7TypeName"},"type":"array"}]},"enum":{"items":{"$ref":"#/components/schemas/JSONSchema7Type"},"type":"array"},"const":{"$ref":"#/components/schemas/JSONSchema7Type"},"multipleOf":{"type":"number","format":"double"},"maximum":{"type":"number","format":"double"},"exclusiveMaximum":{"type":"number","format":"double"},"minimum":{"type":"number","format":"double"},"exclusiveMinimum":{"type":"number","format":"double"},"maxLength":{"type":"number","format":"double"},"minLength":{"type":"number","format":"double"},"pattern":{"type":"string"},"items":{"anyOf":[{"$ref":"#/components/schemas/JSONSchema7Definition"},{"items":{"$ref":"#/components/schemas/JSONSchema7Definition"},"type":"array"}]},"additionalItems":{"$ref":"#/components/schemas/JSONSchema7Definition"},"maxItems":{"type":"number","format":"double"},"minItems":{"type":"number","format":"double"},"uniqueItems":{"type":"boolean"},"contains":{"$ref":"#/components/schemas/JSONSchema7Definition"},"maxProperties":{"type":"number","format":"double"},"minProperties":{"type":"number","format":"double"},"required":{"items":{"type":"string"},"type":"array"},"properties":{"properties":{},"additionalProperties":{"$ref":"#/components/schemas/JSONSchema7Definition"},"type":"object"},"patternProperties":{"properties":{},"additionalProperties":{"$ref":"#/components/schemas/JSONSchema7Definition"},"type":"object"},"additionalProperties":{"$ref":"#/components/schemas/JSONSchema7Definition"},"dependencies":{"properties":{},"additionalProperties":{"anyOf":[{"$ref":"#/components/schemas/JSONSchema7Definition"},{"items":{"type":"string"},"type":"array"}]},"type":"object"},"propertyNames":{"$ref":"#/components/schemas/JSONSchema7Definition"},"if":{"$ref":"#/components/schemas/JSONSchema7Definition"},"then":{"$ref":"#/components/schemas/JSONSchema7Definition"},"else":{"$ref":"#/components/schemas/JSONSchema7Definition"},"allOf":{"items":{"$ref":"#/components/schemas/JSONSchema7Definition"},"type":"array"},"anyOf":{"items":{"$ref":"#/components/schemas/JSONSchema7Definition"},"type":"array"},"oneOf":{"items":{"$ref":"#/components/schemas/JSONSchema7Definition"},"type":"array"},"not":{"$ref":"#/components/schemas/JSONSchema7Definition"},"format":{"type":"string"},"contentMediaType":{"type":"string"},"contentEncoding":{"type":"string"},"definitions":{"properties":{},"additionalProperties":{"$ref":"#/components/schemas/JSONSchema7Definition"},"type":"object"},"title":{"type":"string"},"description":{"type":"string"},"default":{"$ref":"#/components/schemas/JSONSchema7Type"},"readOnly":{"type":"boolean"},"writeOnly":{"type":"boolean"}},"type":"object","additionalProperties":false},"JSONSchema7Version":{"type":"string","description":"Meta schema\n\nRecommended values:\n- 'http://json-schema.org/schema#'\n- 'http://json-schema.org/hyper-schema#'\n- 'http://json-schema.org/draft-07/schema#'\n- 'http://json-schema.org/draft-07/hyper-schema#'"},"JSONSchema7Definition":{"anyOf":[{"$ref":"#/components/schemas/JSONSchema7"},{"type":"boolean"}],"description":"JSON Schema v7"},"JSONSchema7TypeName":{"type":"string","enum":["string","number","integer","boolean","object","array","null"],"description":"Primitive type"},"JSONSchema7Type":{"anyOf":[{"type":"string"},{"type":"number","format":"double"},{"type":"boolean"},{"$ref":"#/components/schemas/JSONSchema7Object"},{"$ref":"#/components/schemas/JSONSchema7Array"}],"nullable":true,"description":"Primitive type"},"JSONSchema7Object":{"properties":{},"type":"object","additionalProperties":{"$ref":"#/components/schemas/JSONSchema7Type"}},"JSONSchema7Array":{"properties":{},"type":"object","additionalProperties":false}}},"paths":{"/api/v1/verifier/vp-queries":{"get":{"operationId":"GetVpQueries","responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/VpQueryConfiguration"},"type":"array"}}}},"400":{"description":"Bad Request","content":{"application/json":{"schema":{}}}}},"description":"Retrieves all VP query configurations.","tags":["VP Query"],"parameters":[]}}}}
```

## POST /api/v1/verifier/vp-queries

> Creates a new VP query configuration.

```json
{"openapi":"3.0.0","info":{"title":"verifier-service","version":"1.0.0-rc1"},"servers":[{"url":"/"}],"security":[{"clientSecret":[]}],"components":{"securitySchemes":{"clientSecret":{"type":"apiKey","name":"x-client-secret","in":"header"}},"schemas":{"VpQueryConfiguration":{"description":"Entity representing a Verifiable Presentation (VP) query configuration.\n\nVP queries define what credentials a verifier is requesting from a user.\nThese configurations can be reused across multiple authorization requests,\nallowing for standardized verification processes.","properties":{"id":{"type":"string","description":"Unique identifier for the VP query configuration.\nAutomatically generated as a UUID."},"queryBody":{"items":{"$ref":"#/components/schemas/VPQueryParams"},"type":"array","description":"The body of the VP query as a JSON array.\nContains the parameters that define what credentials are being requested,\nincluding types, formats, and constraints."},"createdAt":{"type":"number","format":"double","description":"Timestamp when this VP query configuration was created.\nStored as a bigint representing milliseconds since the Unix epoch."}},"required":["id","queryBody"],"type":"object","additionalProperties":false},"VPQueryParams":{"properties":{"fields":{"items":{"$ref":"#/components/schemas/InputDescriptorField"},"type":"array","description":"List of descriptor fields that define paths and optional filters"}},"required":["fields"],"type":"object","description":"Parameters for querying Verifiable Presentations.\nContains fields to specify which parts of a credential to query\nand optional filters to apply."},"InputDescriptorField":{"properties":{"filter":{"$ref":"#/components/schemas/JSONSchema7","description":"Optional JSON Schema that values must conform to"},"path":{"items":{"type":"string"},"type":"array","description":"Array of JSONPath expressions to locate values in the credential"}},"required":["path"],"type":"object","description":"Describes a field within an input descriptor.\nUsed to specify which paths to extract from a credential\nand optional JSON Schema filter to validate the values."},"JSONSchema7":{"properties":{"$id":{"type":"string"},"$ref":{"type":"string"},"$schema":{"$ref":"#/components/schemas/JSONSchema7Version"},"$comment":{"type":"string"},"$defs":{"properties":{},"additionalProperties":{"$ref":"#/components/schemas/JSONSchema7Definition"},"type":"object"},"type":{"anyOf":[{"$ref":"#/components/schemas/JSONSchema7TypeName"},{"items":{"$ref":"#/components/schemas/JSONSchema7TypeName"},"type":"array"}]},"enum":{"items":{"$ref":"#/components/schemas/JSONSchema7Type"},"type":"array"},"const":{"$ref":"#/components/schemas/JSONSchema7Type"},"multipleOf":{"type":"number","format":"double"},"maximum":{"type":"number","format":"double"},"exclusiveMaximum":{"type":"number","format":"double"},"minimum":{"type":"number","format":"double"},"exclusiveMinimum":{"type":"number","format":"double"},"maxLength":{"type":"number","format":"double"},"minLength":{"type":"number","format":"double"},"pattern":{"type":"string"},"items":{"anyOf":[{"$ref":"#/components/schemas/JSONSchema7Definition"},{"items":{"$ref":"#/components/schemas/JSONSchema7Definition"},"type":"array"}]},"additionalItems":{"$ref":"#/components/schemas/JSONSchema7Definition"},"maxItems":{"type":"number","format":"double"},"minItems":{"type":"number","format":"double"},"uniqueItems":{"type":"boolean"},"contains":{"$ref":"#/components/schemas/JSONSchema7Definition"},"maxProperties":{"type":"number","format":"double"},"minProperties":{"type":"number","format":"double"},"required":{"items":{"type":"string"},"type":"array"},"properties":{"properties":{},"additionalProperties":{"$ref":"#/components/schemas/JSONSchema7Definition"},"type":"object"},"patternProperties":{"properties":{},"additionalProperties":{"$ref":"#/components/schemas/JSONSchema7Definition"},"type":"object"},"additionalProperties":{"$ref":"#/components/schemas/JSONSchema7Definition"},"dependencies":{"properties":{},"additionalProperties":{"anyOf":[{"$ref":"#/components/schemas/JSONSchema7Definition"},{"items":{"type":"string"},"type":"array"}]},"type":"object"},"propertyNames":{"$ref":"#/components/schemas/JSONSchema7Definition"},"if":{"$ref":"#/components/schemas/JSONSchema7Definition"},"then":{"$ref":"#/components/schemas/JSONSchema7Definition"},"else":{"$ref":"#/components/schemas/JSONSchema7Definition"},"allOf":{"items":{"$ref":"#/components/schemas/JSONSchema7Definition"},"type":"array"},"anyOf":{"items":{"$ref":"#/components/schemas/JSONSchema7Definition"},"type":"array"},"oneOf":{"items":{"$ref":"#/components/schemas/JSONSchema7Definition"},"type":"array"},"not":{"$ref":"#/components/schemas/JSONSchema7Definition"},"format":{"type":"string"},"contentMediaType":{"type":"string"},"contentEncoding":{"type":"string"},"definitions":{"properties":{},"additionalProperties":{"$ref":"#/components/schemas/JSONSchema7Definition"},"type":"object"},"title":{"type":"string"},"description":{"type":"string"},"default":{"$ref":"#/components/schemas/JSONSchema7Type"},"readOnly":{"type":"boolean"},"writeOnly":{"type":"boolean"}},"type":"object","additionalProperties":false},"JSONSchema7Version":{"type":"string","description":"Meta schema\n\nRecommended values:\n- 'http://json-schema.org/schema#'\n- 'http://json-schema.org/hyper-schema#'\n- 'http://json-schema.org/draft-07/schema#'\n- 'http://json-schema.org/draft-07/hyper-schema#'"},"JSONSchema7Definition":{"anyOf":[{"$ref":"#/components/schemas/JSONSchema7"},{"type":"boolean"}],"description":"JSON Schema v7"},"JSONSchema7TypeName":{"type":"string","enum":["string","number","integer","boolean","object","array","null"],"description":"Primitive type"},"JSONSchema7Type":{"anyOf":[{"type":"string"},{"type":"number","format":"double"},{"type":"boolean"},{"$ref":"#/components/schemas/JSONSchema7Object"},{"$ref":"#/components/schemas/JSONSchema7Array"}],"nullable":true,"description":"Primitive type"},"JSONSchema7Object":{"properties":{},"type":"object","additionalProperties":{"$ref":"#/components/schemas/JSONSchema7Type"}},"JSONSchema7Array":{"properties":{},"type":"object","additionalProperties":false},"CreateVpQueryRequestDto":{"description":"DTO for creating a VP (Verifiable Presentation) query\n\nContains validation to ensure the input data is a properly formatted\nquery for verifying presentations (VP Query).","properties":{"query":{"items":{"$ref":"#/components/schemas/VPQueryParams"},"type":"array","description":"Array of VPQueryParams"}},"required":["query"],"type":"object","additionalProperties":false}}},"paths":{"/api/v1/verifier/vp-queries":{"post":{"operationId":"CreateVpQuery","responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VpQueryConfiguration"}}}},"400":{"description":"Bad Request","content":{"application/json":{"schema":{}}}}},"description":"Creates a new VP query configuration.","tags":["VP Query"],"parameters":[],"requestBody":{"description":"- Contains the query parameters for verifiable presentation verification\nThe query property should contain an array of VP query configurations that define\nwhat credentials and claims are required for successful verification.","required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateVpQueryRequestDto","description":"- Contains the query parameters for verifiable presentation verification\nThe query property should contain an array of VP query configurations that define\nwhat credentials and claims are required for successful verification."}}}}}}}}
```

## GET /api/v1/verifier/vp-queries/{id}

> Retrieves a VP query configuration by its ID.

```json
{"openapi":"3.0.0","info":{"title":"verifier-service","version":"1.0.0-rc1"},"servers":[{"url":"/"}],"security":[{"clientSecret":[]}],"components":{"securitySchemes":{"clientSecret":{"type":"apiKey","name":"x-client-secret","in":"header"}},"schemas":{"VpQueryConfiguration":{"description":"Entity representing a Verifiable Presentation (VP) query configuration.\n\nVP queries define what credentials a verifier is requesting from a user.\nThese configurations can be reused across multiple authorization requests,\nallowing for standardized verification processes.","properties":{"id":{"type":"string","description":"Unique identifier for the VP query configuration.\nAutomatically generated as a UUID."},"queryBody":{"items":{"$ref":"#/components/schemas/VPQueryParams"},"type":"array","description":"The body of the VP query as a JSON array.\nContains the parameters that define what credentials are being requested,\nincluding types, formats, and constraints."},"createdAt":{"type":"number","format":"double","description":"Timestamp when this VP query configuration was created.\nStored as a bigint representing milliseconds since the Unix epoch."}},"required":["id","queryBody"],"type":"object","additionalProperties":false},"VPQueryParams":{"properties":{"fields":{"items":{"$ref":"#/components/schemas/InputDescriptorField"},"type":"array","description":"List of descriptor fields that define paths and optional filters"}},"required":["fields"],"type":"object","description":"Parameters for querying Verifiable Presentations.\nContains fields to specify which parts of a credential to query\nand optional filters to apply."},"InputDescriptorField":{"properties":{"filter":{"$ref":"#/components/schemas/JSONSchema7","description":"Optional JSON Schema that values must conform to"},"path":{"items":{"type":"string"},"type":"array","description":"Array of JSONPath expressions to locate values in the credential"}},"required":["path"],"type":"object","description":"Describes a field within an input descriptor.\nUsed to specify which paths to extract from a credential\nand optional JSON Schema filter to validate the values."},"JSONSchema7":{"properties":{"$id":{"type":"string"},"$ref":{"type":"string"},"$schema":{"$ref":"#/components/schemas/JSONSchema7Version"},"$comment":{"type":"string"},"$defs":{"properties":{},"additionalProperties":{"$ref":"#/components/schemas/JSONSchema7Definition"},"type":"object"},"type":{"anyOf":[{"$ref":"#/components/schemas/JSONSchema7TypeName"},{"items":{"$ref":"#/components/schemas/JSONSchema7TypeName"},"type":"array"}]},"enum":{"items":{"$ref":"#/components/schemas/JSONSchema7Type"},"type":"array"},"const":{"$ref":"#/components/schemas/JSONSchema7Type"},"multipleOf":{"type":"number","format":"double"},"maximum":{"type":"number","format":"double"},"exclusiveMaximum":{"type":"number","format":"double"},"minimum":{"type":"number","format":"double"},"exclusiveMinimum":{"type":"number","format":"double"},"maxLength":{"type":"number","format":"double"},"minLength":{"type":"number","format":"double"},"pattern":{"type":"string"},"items":{"anyOf":[{"$ref":"#/components/schemas/JSONSchema7Definition"},{"items":{"$ref":"#/components/schemas/JSONSchema7Definition"},"type":"array"}]},"additionalItems":{"$ref":"#/components/schemas/JSONSchema7Definition"},"maxItems":{"type":"number","format":"double"},"minItems":{"type":"number","format":"double"},"uniqueItems":{"type":"boolean"},"contains":{"$ref":"#/components/schemas/JSONSchema7Definition"},"maxProperties":{"type":"number","format":"double"},"minProperties":{"type":"number","format":"double"},"required":{"items":{"type":"string"},"type":"array"},"properties":{"properties":{},"additionalProperties":{"$ref":"#/components/schemas/JSONSchema7Definition"},"type":"object"},"patternProperties":{"properties":{},"additionalProperties":{"$ref":"#/components/schemas/JSONSchema7Definition"},"type":"object"},"additionalProperties":{"$ref":"#/components/schemas/JSONSchema7Definition"},"dependencies":{"properties":{},"additionalProperties":{"anyOf":[{"$ref":"#/components/schemas/JSONSchema7Definition"},{"items":{"type":"string"},"type":"array"}]},"type":"object"},"propertyNames":{"$ref":"#/components/schemas/JSONSchema7Definition"},"if":{"$ref":"#/components/schemas/JSONSchema7Definition"},"then":{"$ref":"#/components/schemas/JSONSchema7Definition"},"else":{"$ref":"#/components/schemas/JSONSchema7Definition"},"allOf":{"items":{"$ref":"#/components/schemas/JSONSchema7Definition"},"type":"array"},"anyOf":{"items":{"$ref":"#/components/schemas/JSONSchema7Definition"},"type":"array"},"oneOf":{"items":{"$ref":"#/components/schemas/JSONSchema7Definition"},"type":"array"},"not":{"$ref":"#/components/schemas/JSONSchema7Definition"},"format":{"type":"string"},"contentMediaType":{"type":"string"},"contentEncoding":{"type":"string"},"definitions":{"properties":{},"additionalProperties":{"$ref":"#/components/schemas/JSONSchema7Definition"},"type":"object"},"title":{"type":"string"},"description":{"type":"string"},"default":{"$ref":"#/components/schemas/JSONSchema7Type"},"readOnly":{"type":"boolean"},"writeOnly":{"type":"boolean"}},"type":"object","additionalProperties":false},"JSONSchema7Version":{"type":"string","description":"Meta schema\n\nRecommended values:\n- 'http://json-schema.org/schema#'\n- 'http://json-schema.org/hyper-schema#'\n- 'http://json-schema.org/draft-07/schema#'\n- 'http://json-schema.org/draft-07/hyper-schema#'"},"JSONSchema7Definition":{"anyOf":[{"$ref":"#/components/schemas/JSONSchema7"},{"type":"boolean"}],"description":"JSON Schema v7"},"JSONSchema7TypeName":{"type":"string","enum":["string","number","integer","boolean","object","array","null"],"description":"Primitive type"},"JSONSchema7Type":{"anyOf":[{"type":"string"},{"type":"number","format":"double"},{"type":"boolean"},{"$ref":"#/components/schemas/JSONSchema7Object"},{"$ref":"#/components/schemas/JSONSchema7Array"}],"nullable":true,"description":"Primitive type"},"JSONSchema7Object":{"properties":{},"type":"object","additionalProperties":{"$ref":"#/components/schemas/JSONSchema7Type"}},"JSONSchema7Array":{"properties":{},"type":"object","additionalProperties":false}}},"paths":{"/api/v1/verifier/vp-queries/{id}":{"get":{"operationId":"GetVpQueryById","responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VpQueryConfiguration"}}}},"400":{"description":"Bad Request","content":{"application/json":{"schema":{}}}},"404":{"description":"Not Found"}},"description":"Retrieves a VP query configuration by its ID.","tags":["VP Query"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}]}}}}
```

## DELETE /api/v1/verifier/vp-queries/{id}

> Deletes a VP query configuration by its ID.

```json
{"openapi":"3.0.0","info":{"title":"verifier-service","version":"1.0.0-rc1"},"servers":[{"url":"/"}],"security":[{"clientSecret":[]}],"components":{"securitySchemes":{"clientSecret":{"type":"apiKey","name":"x-client-secret","in":"header"}}},"paths":{"/api/v1/verifier/vp-queries/{id}":{"delete":{"operationId":"DeleteVpQuery","responses":{"204":{"description":"No Content"},"400":{"description":"Bad Request","content":{"application/json":{"schema":{}}}}},"description":"Deletes a VP query configuration by its ID.","tags":["VP Query"],"parameters":[{"description":"- The UUID of the VP query configuration to delete","in":"path","name":"id","required":true,"schema":{"type":"string"}}]}}}}
```

## POST /api/v1/verifier/vp-queries/{id}/authorization-request

> Handles the creation of an authorization request using a VP Query.\
> Retrieves the presentation definition from the specified VP query and creates an authorization request.

```json
{"openapi":"3.0.0","info":{"title":"verifier-service","version":"1.0.0-rc1"},"servers":[{"url":"/"}],"security":[{"clientSecret":[]}],"components":{"securitySchemes":{"clientSecret":{"type":"apiKey","name":"x-client-secret","in":"header"}},"schemas":{"CreateAuthorizationRequestResponseDto":{"description":"Response for a successful authorization request creation","properties":{"request_uri":{"type":"string","description":"Request URI that can be used to create a QR code"},"qr_code_url":{"type":"string","description":"URL of the QR code that can be scanned by wallets"},"state":{"type":"string","description":"Random string to prevent CSRF attacks"},"nonce":{"type":"string","description":"Random string to ensure request uniqueness"},"validity":{"type":"number","format":"double","description":"Validity of the authorization request in seconds"}},"required":["request_uri","qr_code_url","state","nonce","validity"],"type":"object","additionalProperties":false},"CreateVpQueryAuthorizationRequestDto":{"description":"Request body for creating an authorization request using a VP query","properties":{"redirect_uri":{"type":"string","description":"Redirect URI to which the response will be sent"},"validity":{"type":"number","format":"double","description":"Validity of the authorization request in seconds"}},"required":["redirect_uri"],"type":"object","additionalProperties":false}}},"paths":{"/api/v1/verifier/vp-queries/{id}/authorization-request":{"post":{"operationId":"CreateVpQueryAuthorizationRequest","responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAuthorizationRequestResponseDto"}}}},"400":{"description":"Bad Request","content":{"application/json":{"schema":{}}}}},"description":"Handles the creation of an authorization request using a VP Query.\nRetrieves the presentation definition from the specified VP query and creates an authorization request.","tags":["VP Query"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateVpQueryAuthorizationRequestDto"}}}}}}}}
```


# QR Code

## GET /api/v1/verifier/qr-codes/{id}

> Retrieves a QR code by its ID.\
> \
> Returns the QR code data as a string, which contains a data URI with base64-encoded\
> image data. This QR code encodes a URL pointing to an authorization request that\
> specifies what credentials a verifier is requesting from a user's wallet.

```json
{"openapi":"3.0.0","info":{"title":"verifier-service","version":"1.0.0-rc1"},"servers":[{"url":"/"}],"security":[],"paths":{"/api/v1/verifier/qr-codes/{id}":{"get":{"operationId":"GetQrCodeById","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"string"}}}},"400":{"description":"Bad Request","content":{"application/json":{"schema":{}}}}},"description":"Retrieves a QR code by its ID.\n\nReturns the QR code data as a string, which contains a data URI with base64-encoded\nimage data. This QR code encodes a URL pointing to an authorization request that\nspecifies what credentials a verifier is requesting from a user's wallet.","tags":["QR Code"],"parameters":[{"description":"- The unique identifier of the QR code to retrieve","in":"path","name":"id","required":true,"schema":{"type":"string"}}]}}}}
```

## DELETE /api/v1/verifier/qr-codes/{id}

> Deletes a QR code by its ID.\
> \
> QR codes are typically deleted after the authorization process is complete\
> and the verification flow has concluded, as they are no longer needed.

```json
{"openapi":"3.0.0","info":{"title":"verifier-service","version":"1.0.0-rc1"},"servers":[{"url":"/"}],"security":[{"clientSecret":[]}],"components":{"securitySchemes":{"clientSecret":{"type":"apiKey","name":"x-client-secret","in":"header"}}},"paths":{"/api/v1/verifier/qr-codes/{id}":{"delete":{"operationId":"DeleteQrCode","responses":{"204":{"description":"No Content"},"400":{"description":"Bad Request","content":{"application/json":{"schema":{}}}}},"description":"Deletes a QR code by its ID.\n\nQR codes are typically deleted after the authorization process is complete\nand the verification flow has concluded, as they are no longer needed.","tags":["QR Code"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}]}}}}
```

## GET /api/v1/verifier/qr-codes/image

> Displays a QR code image from a URL.\
> Fetches the QR code data from the provided URL, processes it, and returns the image.\
> \
> This endpoint allows client applications to display the QR code directly in browsers\
> or applications. When scanned by a user's wallet app, this QR code initiates the\
> credential verification flow by connecting the wallet to the authorization request.\
> The wallet can then retrieve the presentation definition that specifies what\
> credentials the verifier is requesting.

```json
{"openapi":"3.0.0","info":{"title":"verifier-service","version":"1.0.0-rc1"},"servers":[{"url":"/"}],"security":[],"paths":{"/api/v1/verifier/qr-codes/image":{"get":{"operationId":"GetQrCodeAsImage","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"string","format":"byte"}}}}},"description":"Displays a QR code image from a URL.\nFetches the QR code data from the provided URL, processes it, and returns the image.\n\nThis endpoint allows client applications to display the QR code directly in browsers\nor applications. When scanned by a user's wallet app, this QR code initiates the\ncredential verification flow by connecting the wallet to the authorization request.\nThe wallet can then retrieve the presentation definition that specifies what\ncredentials the verifier is requesting.","tags":["QR Code"],"parameters":[{"in":"query","name":"url","required":true,"schema":{"type":"string"}}]}}}}
```


# Authorization

## POST /api/v1/verifier/authorization-requests

> Handles the creation of an authorization request.\
> Creates a new authorization request with the provided presentation definition.

```json
{"openapi":"3.0.0","info":{"title":"verifier-service","version":"1.0.0-rc1"},"servers":[{"url":"/"}],"security":[{"clientSecret":[]}],"components":{"securitySchemes":{"clientSecret":{"type":"apiKey","name":"x-client-secret","in":"header"}},"schemas":{"CreateAuthorizationRequestResponseDto":{"description":"Response for a successful authorization request creation","properties":{"request_uri":{"type":"string","description":"Request URI that can be used to create a QR code"},"qr_code_url":{"type":"string","description":"URL of the QR code that can be scanned by wallets"},"state":{"type":"string","description":"Random string to prevent CSRF attacks"},"nonce":{"type":"string","description":"Random string to ensure request uniqueness"},"validity":{"type":"number","format":"double","description":"Validity of the authorization request in seconds"}},"required":["request_uri","qr_code_url","state","nonce","validity"],"type":"object","additionalProperties":false},"CreateAuthorizationRequestDto":{"description":"Request body for creating an authorization request","properties":{"redirect_uri":{"type":"string","description":"Redirect URI to which the response will be sent"},"presentation_definition":{"$ref":"#/components/schemas/PresentationDefinition","description":"The presentation definition that defines what credentials are required"},"validity":{"type":"number","format":"double","description":"Validity of the authorization request in seconds"}},"required":["redirect_uri","presentation_definition"],"type":"object","additionalProperties":false},"PresentationDefinition":{"properties":{"input_descriptors":{"items":{"$ref":"#/components/schemas/InputDescriptor"},"type":"array","description":"List of input descriptors defining required credentials"},"id":{"type":"string","description":"Unique identifier for this presentation definition"}},"required":["input_descriptors","id"],"type":"object","description":"Defines requirements for a verifiable presentation.\nUsed by verifiers to specify what credentials they require."},"InputDescriptor":{"properties":{"constraints":{"properties":{"fields":{"items":{"$ref":"#/components/schemas/InputDescriptorField"},"type":"array","description":"Fields that must be present in the credential"},"limit_disclosure":{"type":"string","enum":["required"],"nullable":false,"description":"Defines disclosure requirements for credential fields"}},"required":["fields","limit_disclosure"],"type":"object","description":"Constraints that credentials must satisfy"},"id":{"type":"string","description":"Unique identifier for this input descriptor"}},"required":["constraints","id"],"type":"object","description":"Describes requirements for a credential in a presentation.\nUsed to define constraints on what credentials are acceptable\nfor a given verification purpose."},"InputDescriptorField":{"properties":{"filter":{"$ref":"#/components/schemas/JSONSchema7","description":"Optional JSON Schema that values must conform to"},"path":{"items":{"type":"string"},"type":"array","description":"Array of JSONPath expressions to locate values in the credential"}},"required":["path"],"type":"object","description":"Describes a field within an input descriptor.\nUsed to specify which paths to extract from a credential\nand optional JSON Schema filter to validate the values."},"JSONSchema7":{"properties":{"$id":{"type":"string"},"$ref":{"type":"string"},"$schema":{"$ref":"#/components/schemas/JSONSchema7Version"},"$comment":{"type":"string"},"$defs":{"properties":{},"additionalProperties":{"$ref":"#/components/schemas/JSONSchema7Definition"},"type":"object"},"type":{"anyOf":[{"$ref":"#/components/schemas/JSONSchema7TypeName"},{"items":{"$ref":"#/components/schemas/JSONSchema7TypeName"},"type":"array"}]},"enum":{"items":{"$ref":"#/components/schemas/JSONSchema7Type"},"type":"array"},"const":{"$ref":"#/components/schemas/JSONSchema7Type"},"multipleOf":{"type":"number","format":"double"},"maximum":{"type":"number","format":"double"},"exclusiveMaximum":{"type":"number","format":"double"},"minimum":{"type":"number","format":"double"},"exclusiveMinimum":{"type":"number","format":"double"},"maxLength":{"type":"number","format":"double"},"minLength":{"type":"number","format":"double"},"pattern":{"type":"string"},"items":{"anyOf":[{"$ref":"#/components/schemas/JSONSchema7Definition"},{"items":{"$ref":"#/components/schemas/JSONSchema7Definition"},"type":"array"}]},"additionalItems":{"$ref":"#/components/schemas/JSONSchema7Definition"},"maxItems":{"type":"number","format":"double"},"minItems":{"type":"number","format":"double"},"uniqueItems":{"type":"boolean"},"contains":{"$ref":"#/components/schemas/JSONSchema7Definition"},"maxProperties":{"type":"number","format":"double"},"minProperties":{"type":"number","format":"double"},"required":{"items":{"type":"string"},"type":"array"},"properties":{"properties":{},"additionalProperties":{"$ref":"#/components/schemas/JSONSchema7Definition"},"type":"object"},"patternProperties":{"properties":{},"additionalProperties":{"$ref":"#/components/schemas/JSONSchema7Definition"},"type":"object"},"additionalProperties":{"$ref":"#/components/schemas/JSONSchema7Definition"},"dependencies":{"properties":{},"additionalProperties":{"anyOf":[{"$ref":"#/components/schemas/JSONSchema7Definition"},{"items":{"type":"string"},"type":"array"}]},"type":"object"},"propertyNames":{"$ref":"#/components/schemas/JSONSchema7Definition"},"if":{"$ref":"#/components/schemas/JSONSchema7Definition"},"then":{"$ref":"#/components/schemas/JSONSchema7Definition"},"else":{"$ref":"#/components/schemas/JSONSchema7Definition"},"allOf":{"items":{"$ref":"#/components/schemas/JSONSchema7Definition"},"type":"array"},"anyOf":{"items":{"$ref":"#/components/schemas/JSONSchema7Definition"},"type":"array"},"oneOf":{"items":{"$ref":"#/components/schemas/JSONSchema7Definition"},"type":"array"},"not":{"$ref":"#/components/schemas/JSONSchema7Definition"},"format":{"type":"string"},"contentMediaType":{"type":"string"},"contentEncoding":{"type":"string"},"definitions":{"properties":{},"additionalProperties":{"$ref":"#/components/schemas/JSONSchema7Definition"},"type":"object"},"title":{"type":"string"},"description":{"type":"string"},"default":{"$ref":"#/components/schemas/JSONSchema7Type"},"readOnly":{"type":"boolean"},"writeOnly":{"type":"boolean"}},"type":"object","additionalProperties":false},"JSONSchema7Version":{"type":"string","description":"Meta schema\n\nRecommended values:\n- 'http://json-schema.org/schema#'\n- 'http://json-schema.org/hyper-schema#'\n- 'http://json-schema.org/draft-07/schema#'\n- 'http://json-schema.org/draft-07/hyper-schema#'"},"JSONSchema7Definition":{"anyOf":[{"$ref":"#/components/schemas/JSONSchema7"},{"type":"boolean"}],"description":"JSON Schema v7"},"JSONSchema7TypeName":{"type":"string","enum":["string","number","integer","boolean","object","array","null"],"description":"Primitive type"},"JSONSchema7Type":{"anyOf":[{"type":"string"},{"type":"number","format":"double"},{"type":"boolean"},{"$ref":"#/components/schemas/JSONSchema7Object"},{"$ref":"#/components/schemas/JSONSchema7Array"}],"nullable":true,"description":"Primitive type"},"JSONSchema7Object":{"properties":{},"type":"object","additionalProperties":{"$ref":"#/components/schemas/JSONSchema7Type"}},"JSONSchema7Array":{"properties":{},"type":"object","additionalProperties":false}}},"paths":{"/api/v1/verifier/authorization-requests":{"post":{"operationId":"CreateAuthorizationRequest","responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAuthorizationRequestResponseDto"}}}},"400":{"description":"Bad Request","content":{"application/json":{"schema":{}}}}},"description":"Handles the creation of an authorization request.\nCreates a new authorization request with the provided presentation definition.","tags":["Authorization"],"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAuthorizationRequestDto"}}}}}}}}
```

## GET /api/v1/verifier/authorization-requests/{id}

> Retrieves an authorization request by ID.\
> Returns the details of the specified authorization request.

```json
{"openapi":"3.0.0","info":{"title":"verifier-service","version":"1.0.0-rc1"},"servers":[{"url":"/"}],"security":[],"paths":{"/api/v1/verifier/authorization-requests/{id}":{"get":{"operationId":"GetAuthorizationRequestById","responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthorizationRequest"}}}},"400":{"description":"Bad Request","content":{"application/json":{"schema":{}}}}},"description":"Retrieves an authorization request by ID.\nReturns the details of the specified authorization request.","tags":["Authorization"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}]}}},"components":{"schemas":{"AuthorizationRequest":{"description":"Entity representing an authorization request in the verifier service.\n\nAn authorization request is created when a client requests verification of credentials.\nIt contains all the necessary information to process a verifiable presentation,\nincluding the presentation definition that specifies what credentials are required,\nand state/nonce values for security purposes.","properties":{"id":{"type":"string","description":"Unique identifier for the authorization request.\nAutomatically generated as a UUID."},"state":{"type":"string","description":"State parameter used for SIOP (Self-Issued OpenID Provider) flow.\nHelps prevent cross-site request forgery attacks by maintaining state between requests.\nMust be unique when combined with nonce."},"nonce":{"type":"string","description":"Nonce (number used once) parameter for the authorization request.\nProvides replay protection and ensures the response is generated for this specific request.\nMust be unique when combined with state."},"redirect_uri":{"type":"string","description":"URI where the client should be redirected after the verification process.\nThe verification result will be sent to this URI."},"validity":{"type":"number","format":"double","description":"Timestamp (in milliseconds) until which this authorization request is valid.\nAfter this time, the request will be considered expired and cannot be used."},"response_uri":{"type":"string","description":"URI where the wallet should submit the verifiable presentation.\nThis is the endpoint that will receive and process the VP from the wallet."},"presentation_definition":{"$ref":"#/components/schemas/PresentationDefinition","description":"The presentation definition that specifies what credentials are required.\nThis JSON structure defines the requirements for the verifiable presentation\nthat needs to be submitted, including credential types and constraints."},"used":{"type":"boolean","description":"Flag indicating whether this authorization request has been used.\nPrevents the same authorization request from being used multiple times.\nSet to true after a successful verification process.","default":false}},"required":["id","state","nonce","redirect_uri","validity","response_uri","presentation_definition"],"type":"object","additionalProperties":false},"PresentationDefinition":{"properties":{"input_descriptors":{"items":{"$ref":"#/components/schemas/InputDescriptor"},"type":"array","description":"List of input descriptors defining required credentials"},"id":{"type":"string","description":"Unique identifier for this presentation definition"}},"required":["input_descriptors","id"],"type":"object","description":"Defines requirements for a verifiable presentation.\nUsed by verifiers to specify what credentials they require."},"InputDescriptor":{"properties":{"constraints":{"properties":{"fields":{"items":{"$ref":"#/components/schemas/InputDescriptorField"},"type":"array","description":"Fields that must be present in the credential"},"limit_disclosure":{"type":"string","enum":["required"],"nullable":false,"description":"Defines disclosure requirements for credential fields"}},"required":["fields","limit_disclosure"],"type":"object","description":"Constraints that credentials must satisfy"},"id":{"type":"string","description":"Unique identifier for this input descriptor"}},"required":["constraints","id"],"type":"object","description":"Describes requirements for a credential in a presentation.\nUsed to define constraints on what credentials are acceptable\nfor a given verification purpose."},"InputDescriptorField":{"properties":{"filter":{"$ref":"#/components/schemas/JSONSchema7","description":"Optional JSON Schema that values must conform to"},"path":{"items":{"type":"string"},"type":"array","description":"Array of JSONPath expressions to locate values in the credential"}},"required":["path"],"type":"object","description":"Describes a field within an input descriptor.\nUsed to specify which paths to extract from a credential\nand optional JSON Schema filter to validate the values."},"JSONSchema7":{"properties":{"$id":{"type":"string"},"$ref":{"type":"string"},"$schema":{"$ref":"#/components/schemas/JSONSchema7Version"},"$comment":{"type":"string"},"$defs":{"properties":{},"additionalProperties":{"$ref":"#/components/schemas/JSONSchema7Definition"},"type":"object"},"type":{"anyOf":[{"$ref":"#/components/schemas/JSONSchema7TypeName"},{"items":{"$ref":"#/components/schemas/JSONSchema7TypeName"},"type":"array"}]},"enum":{"items":{"$ref":"#/components/schemas/JSONSchema7Type"},"type":"array"},"const":{"$ref":"#/components/schemas/JSONSchema7Type"},"multipleOf":{"type":"number","format":"double"},"maximum":{"type":"number","format":"double"},"exclusiveMaximum":{"type":"number","format":"double"},"minimum":{"type":"number","format":"double"},"exclusiveMinimum":{"type":"number","format":"double"},"maxLength":{"type":"number","format":"double"},"minLength":{"type":"number","format":"double"},"pattern":{"type":"string"},"items":{"anyOf":[{"$ref":"#/components/schemas/JSONSchema7Definition"},{"items":{"$ref":"#/components/schemas/JSONSchema7Definition"},"type":"array"}]},"additionalItems":{"$ref":"#/components/schemas/JSONSchema7Definition"},"maxItems":{"type":"number","format":"double"},"minItems":{"type":"number","format":"double"},"uniqueItems":{"type":"boolean"},"contains":{"$ref":"#/components/schemas/JSONSchema7Definition"},"maxProperties":{"type":"number","format":"double"},"minProperties":{"type":"number","format":"double"},"required":{"items":{"type":"string"},"type":"array"},"properties":{"properties":{},"additionalProperties":{"$ref":"#/components/schemas/JSONSchema7Definition"},"type":"object"},"patternProperties":{"properties":{},"additionalProperties":{"$ref":"#/components/schemas/JSONSchema7Definition"},"type":"object"},"additionalProperties":{"$ref":"#/components/schemas/JSONSchema7Definition"},"dependencies":{"properties":{},"additionalProperties":{"anyOf":[{"$ref":"#/components/schemas/JSONSchema7Definition"},{"items":{"type":"string"},"type":"array"}]},"type":"object"},"propertyNames":{"$ref":"#/components/schemas/JSONSchema7Definition"},"if":{"$ref":"#/components/schemas/JSONSchema7Definition"},"then":{"$ref":"#/components/schemas/JSONSchema7Definition"},"else":{"$ref":"#/components/schemas/JSONSchema7Definition"},"allOf":{"items":{"$ref":"#/components/schemas/JSONSchema7Definition"},"type":"array"},"anyOf":{"items":{"$ref":"#/components/schemas/JSONSchema7Definition"},"type":"array"},"oneOf":{"items":{"$ref":"#/components/schemas/JSONSchema7Definition"},"type":"array"},"not":{"$ref":"#/components/schemas/JSONSchema7Definition"},"format":{"type":"string"},"contentMediaType":{"type":"string"},"contentEncoding":{"type":"string"},"definitions":{"properties":{},"additionalProperties":{"$ref":"#/components/schemas/JSONSchema7Definition"},"type":"object"},"title":{"type":"string"},"description":{"type":"string"},"default":{"$ref":"#/components/schemas/JSONSchema7Type"},"readOnly":{"type":"boolean"},"writeOnly":{"type":"boolean"}},"type":"object","additionalProperties":false},"JSONSchema7Version":{"type":"string","description":"Meta schema\n\nRecommended values:\n- 'http://json-schema.org/schema#'\n- 'http://json-schema.org/hyper-schema#'\n- 'http://json-schema.org/draft-07/schema#'\n- 'http://json-schema.org/draft-07/hyper-schema#'"},"JSONSchema7Definition":{"anyOf":[{"$ref":"#/components/schemas/JSONSchema7"},{"type":"boolean"}],"description":"JSON Schema v7"},"JSONSchema7TypeName":{"type":"string","enum":["string","number","integer","boolean","object","array","null"],"description":"Primitive type"},"JSONSchema7Type":{"anyOf":[{"type":"string"},{"type":"number","format":"double"},{"type":"boolean"},{"$ref":"#/components/schemas/JSONSchema7Object"},{"$ref":"#/components/schemas/JSONSchema7Array"}],"nullable":true,"description":"Primitive type"},"JSONSchema7Object":{"properties":{},"type":"object","additionalProperties":{"$ref":"#/components/schemas/JSONSchema7Type"}},"JSONSchema7Array":{"properties":{},"type":"object","additionalProperties":false}}}}
```

## DELETE /api/v1/verifier/authorization-requests/{id}

> Handles the deletion of an authorization request by ID.\
> Removes the specified authorization request from the system.

```json
{"openapi":"3.0.0","info":{"title":"verifier-service","version":"1.0.0-rc1"},"servers":[{"url":"/"}],"security":[{"clientSecret":[]}],"components":{"securitySchemes":{"clientSecret":{"type":"apiKey","name":"x-client-secret","in":"header"}}},"paths":{"/api/v1/verifier/authorization-requests/{id}":{"delete":{"operationId":"DeleteAuthorizationRequest","responses":{"204":{"description":"No Content"},"400":{"description":"Bad Request","content":{"application/json":{"schema":{}}}}},"description":"Handles the deletion of an authorization request by ID.\nRemoves the specified authorization request from the system.","tags":["Authorization"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}]}}}}
```

## POST /api/v1/verifier/verifiable-presentations

> Handler for the 'submit-vp' endpoint.\
> Processes a submitted verifiable presentation, validates it against the presentation definition,\
> and sends the verification result to the redirect URI.

```json
{"openapi":"3.0.0","info":{"title":"verifier-service","version":"1.0.0-rc1"},"servers":[{"url":"/"}],"security":[],"paths":{"/api/v1/verifier/verifiable-presentations":{"post":{"operationId":"SubmitVp","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"properties":{"message":{"type":"string"}},"required":["message"],"type":"object"}}}},"400":{"description":"Bad Request","content":{"application/json":{"schema":{}}}}},"description":"Handler for the 'submit-vp' endpoint.\nProcesses a submitted verifiable presentation, validates it against the presentation definition,\nand sends the verification result to the redirect URI.","tags":["Authorization"],"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmitVpRequestDto"}}}}}}},"components":{"schemas":{"SubmitVpRequestDto":{"description":"Request body for submitting a verifiable presentation","properties":{"vp_token":{"$ref":"#/components/schemas/VP","description":"The verifiable presentation containing credentials"},"presentation_submission":{"$ref":"#/components/schemas/PresentationSubmission","description":"The presentation submission that maps credentials to the presentation definition"},"state":{"type":"string","description":"Random string to prevent CSRF attacks"},"nonce":{"type":"string","description":"Random string to ensure request uniqueness"}},"required":["vp_token","presentation_submission","state","nonce"],"type":"object","additionalProperties":false},"VP":{"properties":{"proof":{"$ref":"#/components/schemas/Proof","description":"Optional cryptographic proof"},"holder":{"anyOf":[{"type":"string"},{"properties":{"id":{"type":"string"}},"required":["id"],"type":"object"}],"description":"The entity that holds the presentation"},"id":{"type":"string","description":"Optional unique identifier for the presentation"},"verifiableCredential":{"items":{"$ref":"#/components/schemas/VC"},"type":"array","description":"Array of verifiable credentials included in the presentation"},"type":{"anyOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"}],"description":"Type of the verifiable presentation"},"@context":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"string"}],"description":"JSON-LD context"}},"required":["holder","verifiableCredential","type","@context"],"type":"object","description":"Represents a Verifiable Presentation that contains verifiable credentials.\n\nA Verifiable Presentation (VP) is a data model that packages Verifiable Credentials\nfor sharing with a verifier. It includes cryptographic proof of the holder's control\nover the presented credentials.\n\nThe model follows the W3C Verifiable Presentations Data Model specification and can be\nserialized as JSON-LD."},"Proof":{"properties":{"challange":{"type":"string"},"nonce":{"type":"string"},"jws":{"$ref":"#/components/schemas/Base64"},"proofValue":{"$ref":"#/components/schemas/Base64"},"proofPurpose":{"$ref":"#/components/schemas/ProofPurpose"},"verificationMethod":{"$ref":"#/components/schemas/DIDUri"},"created":{"type":"string"},"type":{"$ref":"#/components/schemas/ProofType"}},"required":["proofPurpose","verificationMethod","created","type"],"type":"object"},"Base64":{"type":"string"},"ProofPurpose":{"type":"string","enum":["assertionMethod","authentication","keyAgreement","contractAgreement","capabilityInvocation","capabilityDelegation"]},"DIDUri":{"type":"string"},"ProofType":{"type":"string","enum":["EcdsaSecp256k1Signature2019","EcdsaSecp256r1Signature2019","SchnorrSecp256k1Signature2019","Ed25519Signature2018","Ed25519Signature2020","JwtProof2020","JsonWebSignature2020"],"nullable":false},"VC":{"$ref":"#/components/schemas/VCv1_1","description":"Current version of the Verifiable Credential format used in the implementation"},"VCv1_1":{"properties":{"evidence":{"additionalProperties":false,"type":"object","description":"Additional supporting information for the claims in the credential"},"credentialStatus":{"items":{"$ref":"#/components/schemas/CredentialStatus"},"type":"array","description":"Credential status information, such as revocation or suspension"},"credentialSchema":{"$ref":"#/components/schemas/CredentialSchema","description":"Schema that defines the structure of the credential subject"},"expirationDate":{"type":"string","description":"Date and time when the credential expires"},"proof":{"$ref":"#/components/schemas/Proof","description":"Cryptographic proof that can be used to detect tampering and verify the issuer"},"credentialSubject":{"anyOf":[{"$ref":"#/components/schemas/CredentialSubject"},{"items":{"$ref":"#/components/schemas/CredentialSubject"},"type":"array"}],"description":"Claims about the subject of the credential"},"issuanceDate":{"type":"string","description":"Date and time when the credential was issued"},"issuer":{"$ref":"#/components/schemas/Issuer","description":"Entity that issued the credential"},"id":{"$ref":"#/components/schemas/URI","description":"Unique identifier for the credential"},"type":{"items":{"type":"string"},"type":"array","description":"Credential type information that defines what the credential represents"},"@context":{"anyOf":[{"items":{"$ref":"#/components/schemas/URI"},"type":"array"},{"$ref":"#/components/schemas/URI"}],"description":"JSON-LD context that defines the vocabulary of the credential"}},"required":["credentialSubject","issuanceDate","issuer","type","@context"],"type":"object","description":"Verifiable Credential format following W3C VC Data Model 1.1 specification."},"CredentialStatus":{"description":"A single entry in a bitstring-based status list, per VC-DATA-MODEL-2.0","properties":{"id":{"$ref":"#/components/schemas/URI"},"type":{"type":"string","enum":["BitstringStatusListEntry"],"nullable":false},"statusPurpose":{"anyOf":[{"$ref":"#/components/schemas/StatusPurpose"},{"type":"string"}]},"statusListIndex":{"type":"number","format":"double"},"statusListCredential":{"$ref":"#/components/schemas/URI"},"statusSize":{"type":"number","format":"double"},"statusMessage":{"items":{"properties":{"message":{"type":"string"},"status":{"type":"string"}},"additionalProperties":{},"required":["message","status"],"type":"object"},"type":"array"}},"required":["type","statusPurpose","statusListIndex","statusListCredential"],"type":"object","additionalProperties":false},"URI":{"type":"string"},"StatusPurpose":{"enum":["refresh","revocation","suspension","message"],"type":"string"},"CredentialSchema":{"properties":{"type":{"type":"string","enum":["JsonSchemaValidator2018"],"nullable":false,"description":"The type of schema validator, currently only supporting JsonSchemaValidator2018"},"id":{"$ref":"#/components/schemas/URI","description":"The identifier for the schema, typically a URI pointing to the schema definition"}},"required":["type","id"],"type":"object","description":"Represents a credential schema reference that defines the structure of claims in a credential.\nFollowing W3C VC Data Model, this provides the means to validate credential contents."},"CredentialSubject":{"$ref":"#/components/schemas/Record_string.unknown_"},"Record_string.unknown_":{"properties":{},"additionalProperties":{},"type":"object","description":"Construct a type with a set of properties K of type T"},"Issuer":{"properties":{"name":{"type":"string"},"id":{"$ref":"#/components/schemas/DIDUri"}},"required":["id"],"type":"object","description":"Normalized representation of an issuer after processing InputIssuer."},"PresentationSubmission":{"properties":{"descriptor_map":{"items":{"$ref":"#/components/schemas/DescriptorMap"},"type":"array","description":"Maps between input descriptors and credentials in the presentation"},"definition_id":{"type":"string","description":"ID of the presentation definition this submission satisfies"},"id":{"type":"string","description":"Unique identifier for this submission"}},"required":["descriptor_map","definition_id","id"],"type":"object","description":"Describes how a presentation satisfies a presentation definition.\nMaps between the input descriptors in a definition and the\ncorresponding credentials in a presentation."},"DescriptorMap":{"properties":{"path_nested":{"properties":{"format":{"type":"string","description":"Format of the nested content"},"path":{"type":"string","description":"JSONPath to nested content"}},"required":["format","path"],"type":"object","description":"Optional nested path information"},"format":{"type":"string","description":"Optional format of the credential"},"path":{"type":"string","description":"JSONPath to the location of the corresponding credential in the presentation"},"id":{"type":"string","description":"ID of the input descriptor this map corresponds to"}},"required":["path","id"],"type":"object","description":"Maps between input descriptors and the corresponding credentials\nin a verifiable presentation."}}}}
```


# Wallet Server

The Wallet Server is a server-side, multi-tenant cloud wallet built into the credential platform's HTTP API. Holder and wallet functionality is a module of the same backend that provides the Issuer and Verifier capabilities, addressed under `/agent/holder` and `/holders/:holderId/*`.

It is designed for:

* Running automated tests and integration pipelines without a mobile wallet
* Powering AI agents that need to act as a holder programmatically
* Simulating realistic wallet interactions with issuers and verifiers

It speaks the same **OpenID4VCI** (issuance) and **OpenID4VP** (presentation) credential protocols a mobile wallet uses, so you can drive the same claim and presentation flows from scripts, CI, or agent frameworks — but as plain HTTP calls.

## What It's For

Mobile wallets are great for human-in-the-loop flows. For automated testing or autonomous agents, you often need a non-interactive, scriptable holder that behaves like a real one. The cloud wallet fills that gap: you create a holder once, then process credential-offer and presentation-request URIs over HTTP instead of scanning QR codes by hand.

## Key Capabilities

* **Multi-tenant holders** — one wallet per user, created via `POST /agent/holder` and listed via `GET /agent/holders`. Each holder is identified by a `holderId` (a UUID) used in every wallet call.
* **Holder DIDs** — created as `did:key` or `did:web` when the holder is created. The DID is bound to the holder and used as the cryptographic subject when claiming and presenting credentials.
* **Single-call URI processing** — `POST /holders/:holderId/process` accepts either a credential-offer URI (issuance) or an OpenID4VP authorization-request URI (presentation) and acts automatically based on the URI scheme.
* **Credential store** — list raw stored credentials (`GET /holders/:holderId/credentials`) or decoded payloads (`GET /holders/:holderId/credentials/decoded`).
* **Credential formats** — the wallet can store and present SD-JWT VC, JWT-VC-JSON, JSON-LD VC, W3C v2 (`vc+jwt` / `vc+sd-jwt`), and mdoc credentials it receives.

Holders are created with `did:key` or `did:web`; see the [FAQ](/develop/wallet-server/faq#does-the-service-create-didempe-or-anchor-dids-on-a-blockchain) for how this relates to `did:empe`.

## Creating a Holder

Create a holder for the authenticated user. Choose `key` for a self-contained `did:key`, or `web` to have a `did:web` document hosted by the service.

**POST** `/agent/holder`

### Request Body

* **method** (string, required) — `key` or `web`.
* **domain** (string) — required when `method` is `web`; a hostname such as `example.com`.
* **label** (string) — optional human-readable name for the wallet.

```bash
curl -X POST https://issuer.example.com/agent/holder \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{ "method": "key", "label": "CI Test Holder" }'
```

### Response Body

* **holderId** (string) — the tenant id used for all subsequent wallet calls.
* **did** (string) — the holder's DID.
* **cryptographicBindingMethodsSupported** (string\[]) — e.g. `["did:key","did:jwk","did:web"]`.
* **credentialSigningAlgValuesSupported** (string\[]) — e.g. `["EdDSA","ES256"]`.
* **didDocument** (object) — present only for `did:web` holders.

```json
{
  "holderId": "594b464b-20bd-495a-9fb6-87328c759629",
  "did": "did:key:z6Mkf...",
  "cryptographicBindingMethodsSupported": ["did:key", "did:jwk", "did:web"],
  "credentialSigningAlgValuesSupported": ["EdDSA", "ES256"]
}
```

List the holders owned by the authenticated user with **GET** `/agent/holders`.

## Processing a URI

A single endpoint handles both issuance and presentation. It inspects the URI scheme and either claims the offered credential(s) into the wallet (`openid-credential-offer://...`) or selects matching credentials and sends a presentation (`openid4vp://...`).

**POST** `/holders/:holderId/process`

### Request Body

* **uri** (string, required) — the credential-offer or authorization-request URI.

```bash
curl -X POST https://issuer.example.com/holders/594b464b-20bd-495a-9fb6-87328c759629/process \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{ "uri": "openid-credential-offer://?credential_offer_uri=..." }'
```

### Response Body

For an issuance URI, claimed credentials are stored and reported:

```json
{ "status": "success", "claimed": 1, "ids": ["<credentialId>"] }
```

For a presentation URI, the wallet selects matching credentials (via DCQL or Presentation Exchange) and submits them:

```json
{ "status": "success", "action": "presentation_sent" }
```

## Listing Credentials

Fetch what the wallet currently holds. Use the raw endpoint for the encoded credential strings/objects, or the decoded endpoint to read the claims directly.

**GET** `/holders/:holderId/credentials`

```json
[
  { "id": "<credentialId>", "format": "dc+sd-jwt", "credential": "eyJ..." }
]
```

**GET** `/holders/:holderId/credentials/decoded`

```json
[
  {
    "id": "<credentialId>",
    "format": "dc+sd-jwt",
    "decoded": {
      "header": { "alg": "EdDSA", "typ": "dc+sd-jwt" },
      "payload": {
        "vct": "https://issuer.example.com/vct/employee-badge",
        "employee_id": "E-1024",
        "full_name": "Ada Lovelace",
        "department": "Engineering"
      },
      "prettyClaims": {
        "vct": "https://issuer.example.com/vct/employee-badge",
        "employee_id": "E-1024",
        "full_name": "Ada Lovelace",
        "department": "Engineering"
      }
    }
  }
]
```

If a credential cannot be decoded, its entry includes an `error` field and a `null` `decoded` value.

## Architecture Overview

* **Holder/tenant management** — create and list per-user holder wallets (`/agent/holder`, `/agent/holders`).
* **URI processing** — single-call issuance and verification handling (`/holders/:holderId/process`).
* **Credential store** — list raw and decoded credentials held by a tenant.
* **DID document hosting** — `did:web` holder documents are served by the service at `GET /:uuid/did.json` (and at `GET /:uuid/.well-known/did.json`).
* **Auth guard** — validates the Bearer token and enforces the `admin` role plus per-holder ownership.

## Multi-Tenancy

A single deployment hosts many logical wallets. Each holder is a separate tenant identified by its `holderId` and is owned by the OIDC user who created it. Isolation is enforced per holder: an ownership check rejects any request for a `holderId` the caller does not own, so a user can only reach their own wallets.

## Authentication

Every wallet endpoint requires a valid OIDC JWT supplied as `Authorization: Bearer <token>`. The token is validated against the configured JWKS endpoint, and the `admin` role (from `realm_access.roles`) is required on both `/agent/*` and `/holders/:holderId/*` routes. For local development, set `AUTH_DISABLED=true` to treat every request as an authenticated admin.

## API Reference

Interactive API documentation is available through Swagger UI at `GET /api-docs`, and the raw OpenAPI specification is served at `GET /api-docs-json`. The platform also exposes a public `GET /` hello endpoint and `GET /version` for basic service checks.


# Terminology and Concepts

This page defines the core terms used throughout the Wallet Server documentation. The audience is assumed to know HTTP APIs, JWTs, and cryptography basics, but not necessarily Self-Sovereign Identity.

**Self-Sovereign Identity (SSI)** A model of digital identity in which control of identifiers and credentials belongs to the subject rather than a central provider. Credentials are issued, held, and presented under the holder's control.

**Decentralized Identifier (DID)** A globally unique, cryptographically verifiable identifier that resolves to a DID Document. The Wallet Server creates and uses two DID methods:

* `did:key` — a self-contained method derived from an Ed25519 public key (`did:key:z6Mk...`). No hosting is required.
* `did:web` — a method whose DID Document is hosted over HTTPS at a path the service controls (`did:web:issuer.example.com:<uuid>`).

This service works with `did:key` and `did:web`; see the [FAQ](/develop/wallet-server/faq#does-the-service-create-didempe-or-anchor-dids-on-a-blockchain) for how this relates to the `did:empe` method of the wider Empeiria ecosystem.

**DID Document** The JSON document that a DID resolves to. It contains the verification methods (public keys), services, and controller information used to verify signatures made by the DID subject. For `did:web`, the service hosts this document itself and serves it over HTTPS.

**Verifiable Credential (VC)** A cryptographically signed set of claims that an issuer asserts about a subject (for example, that an employee belongs to a department). A VC can be verified offline using the issuer's DID Document.

**Holder Tenant (`holderId`)** The unit of wallet isolation. Each holder is a per-user cloud-wallet tenant identified by a `holderId` (a UUID) and owned by an authenticated OIDC user. A holder has its own DID and its own stored credentials. Ownership is enforced, so a user can only access the holders they own. Wallet data is partitioned per holder, not directly by the JWT `sub`.

**Credential Offer URI (OpenID4VCI)** The input a holder consumes to receive a credential. An issuer creates an offer and returns an `openid-credential-offer://...` URI (often encoded as a QR code). The holder submits the URI, and the wallet requests the offered credential(s) and stores them in that single operation — no separate multi-step "claim flow" object is involved.

**Authorization Request URI (OpenID4VP)** The input a holder consumes to present credentials. A verifier creates a request and returns an `openid4vp://...` URI (or an HTTPS authorization request, often encoded as a QR code). When a holder processes this URI, the wallet selects the matching credential(s) — using DCQL or DIF Presentation Exchange — and sends the presentation back to the verifier in a single operation.

**OpenID4VCI** The protocol the Wallet Server speaks to issue credentials into a holder. Issuance uses the pre-authorized code flow, with an optional PIN for additional binding.

**OpenID4VP** The protocol the Wallet Server speaks to request and verify presentations from a holder.

**SD-JWT (Selective Disclosure)** An SD-JWT VC (`vc+sd-jwt`) is a credential format in which individual claims can be selectively disclosed. The issuer marks which claims are selectively disclosable when the credential is created; at presentation time the holder discloses only the claims a verifier needs, keeping the rest private. A credential configuration referencing this format looks like `EmployeeBadge@1:sd-jwt`.

**JWT-VC-JSON** A credential format (`jwt-vc-json`) in which the credential is a JWT carrying the full claim set. Unlike SD-JWT, it does not support per-claim selective disclosure; the whole credential is presented.

**Tenant Scoping** Every non-public request is evaluated in the context of the authenticated OIDC user (a valid `Authorization: Bearer <token>`). Holder and wallet operations are additionally scoped to a specific holder tenant owned by that user, so data is isolated per holder. In local development, `AUTH_DISABLED=true` treats every request as an authenticated user.

**Cryptographic Binding Method** The DID method a holder uses to bind a received credential to its key. A holder is *created* with a `did:key` or `did:web` identifier, but it advertises support for binding via `did:key`, `did:jwk`, and `did:web` (the `cryptographicBindingMethodsSupported` it returns at creation). Binding is what ties an issued credential to the holder's controlled key.


# Holders and Tenancy

The Wallet Server runs as a single backend, but each user gets their own isolated cloud wallet. That wallet is called a **holder**: a per-user tenant that owns a DID and stores the Verifiable Credentials (VCs) belonging to that user. A holder is created once, identified by a `holderId`, and from then on every wallet operation is addressed under `/holders/:holderId/*`.

This page covers how holders are created, listed, and isolated from one another. For acquiring and inspecting the credentials inside a holder, see the Credential Management page.

All endpoints below require a valid OIDC JWT Bearer token (`Authorization: Bearer <token>`); the `/agent/*` management routes and the `/holders/:holderId/*` wallet routes additionally require the **admin** role. In development you can bypass auth with `AUTH_DISABLED=true`, which treats every request as an authenticated admin.

## The holder model

A holder is a self-contained wallet tenant. When you create one, the service:

* provisions a DID for the wallet (either `did:key` or `did:web`),
* generates and stores the wallet's signing key,
* maps the wallet to the calling user, and
* returns a `holderId` you use to address the wallet from then on.

The holder's DID is the identity its credentials are bound to and the identity it presents under. Only `did:web` and `did:key` are created. If you choose `did:web`, the service hosts the DID document itself and serves it over HTTPS; see the DID Document Management page for the served-document details.

## Create a holder

**POST** `/agent/holder`

Creates a per-user cloud-wallet tenant and binds it to the authenticated user. The user is identified by the `sub` claim of the Bearer token, so no user identifier is sent in the body.

### Request Body

* **method** (string, required): `key` or `web`. Selects the DID method for the holder's wallet.
* **domain** (string): the hostname for the DID, without a scheme (e.g. `example.com`, optionally with a port such as `example.com:8080`). Required when `method` is `web`; ignored for `key`.
* **label** (string, optional): a human-readable label for the wallet, useful when a user owns several holders.

```
curl -X POST http://localhost:3000/agent/holder \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{"method":"web","domain":"example.com","label":"User Wallet 1"}'
```

### Response Body

```json
{
  "holderId": "594b464b-20bd-495a-9fb6-87328c759629",
  "did": "did:web:example.com:594b464b-20bd-495a-9fb6-87328c759629",
  "cryptographicBindingMethodsSupported": ["did:key", "did:jwk", "did:web"],
  "credentialSigningAlgValuesSupported": ["EdDSA", "ES256"],
  "didDocument": { "...": "..." }
}
```

* **holderId**: the internal holder identifier (UUID). This is the tenant id used in every `/holders/:holderId/*` path.
* **did**: the holder's DID. A `did:web` DID is built as `did:web:<domain>:<holderId>`; a `did:key` DID looks like `did:key:z6Mk...`.
* **cryptographicBindingMethodsSupported**: the DID binding methods the wallet can use when receiving credentials.
* **credentialSigningAlgValuesSupported**: the signing algorithms the wallet supports.
* **didDocument**: the full DID document JSON. Returned only when `method` is `web`.

For a `did:key` holder, omit `domain`:

```
curl -X POST http://localhost:3000/agent/holder \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{"method":"key","label":"Mobile wallet"}'
```

## List your holders

**GET** `/agent/holders`

Returns the holders owned by the authenticated user, most recent first. The result is scoped to the caller's `sub`: you only ever see your own holders.

```
curl -H 'Authorization: Bearer <token>' \
  http://localhost:3000/agent/holders
```

### Response Body

An array of holder summaries:

```json
[
  {
    "holderId": "594b464b-20bd-495a-9fb6-87328c759629",
    "did": "did:web:example.com:594b464b-20bd-495a-9fb6-87328c759629",
    "label": "User Wallet 1",
    "createdAt": "2026-06-22T09:14:03.000Z"
  }
]
```

* **holderId**: the tenant id used in `/holders/:holderId/*` calls.
* **did**: the holder's DID.
* **label**: the label supplied at creation, or `null` if none was given.
* **createdAt**: when the holder was created.

## Multi-tenancy and ownership

Although every holder lives in the same backend, holders are isolated per user:

* **Ownership is bound to the token subject.** When a holder is created, the service records a mapping between the user (the JWT `sub` claim) and the new `holderId`. `GET /agent/holders` returns only the mappings for the calling user.
* **Wallet routes are ownership-checked.** Every request to a `/holders/:holderId/*` route is verified against this mapping. If the `holderId` in the path is not owned by the caller, the request is rejected with `403 Forbidden` and the standard error body `{ statusCode, message, path, timestamp }` — even though the token is otherwise valid and has the admin role.
* **The `holderId` addresses the wallet.** It is the single handle that ties together the holder's DID, its stored credentials, and its access checks. Use it in every wallet call, for example `POST /holders/:holderId/process` and `GET /holders/:holderId/credentials`.

Because ownership keys off the token subject, a token issued to one user can never reach another user's wallet, and a holder created under `AUTH_DISABLED=true` is owned by the development user that mode injects.

## End-to-end example

A typical sequence for standing up a wallet and putting a credential into it:

1. Create the holder and capture its `holderId`:

```
curl -X POST http://localhost:3000/agent/holder \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{"method":"web","domain":"example.com","label":"Engineering badges"}'
```

2. Process a credential-offer URI to claim a credential into the wallet (here, an `EmployeeBadge@1.0:sd-jwt` offer):

```
curl -X POST http://localhost:3000/holders/594b464b-20bd-495a-9fb6-87328c759629/process \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{"uri": "openid-credential-offer://?credential_offer_uri=..."}'
```

3. List the holder's decoded credentials to confirm it was stored:

```
curl -H 'Authorization: Bearer <token>' \
  http://localhost:3000/holders/594b464b-20bd-495a-9fb6-87328c759629/credentials/decoded
```

For the full claim and presentation walkthroughs, see [Processing Credential Offers and Presentation Requests](/develop/wallet-server/flows-claim-and-presentation).

## Notes

* A holder's DID method is fixed at creation time; there is no endpoint to change a holder's DID afterward.
* The `/agent` holder routes cover creation (`POST /agent/holder`) and listing (`GET /agent/holders`); holder deletion is not currently part of the public API.


# Environment & Setup

*These steps are only required when self-hosting the wallet-server. You can skip this section if you deploy from Empeiria's One-Click Deployment Portal.*

## Prerequisites

* Node.js 20+
* Yarn 1.22.x (the repo pins `yarn@1.22.22` via `packageManager`)
* PostgreSQL 14+ (tested with 16.x)

## Configuration (.env)

Create a `.env` file in the wallet-server root. Configuration is read from environment variables; the service validates the required ones at startup and refuses to boot if any are missing.

### Required

* **`DATABASE_URL`** — PostgreSQL connection string for the datastore, e.g. `postgres://user:pass@localhost:5432/ssi`.
* **`ISSUER_BASE_URL`** — public base URL under which this service serves OpenID4VCI issuer endpoints and credential offers. Any base URL works; by convention it ends with the protocol's path segment, e.g. `https://issuer.example.com/oid4vci` (and `https://verifier.example.com/oid4vp` for the verifier URL below).
* **`VERIFIER_BASE_URL`** — public base URL under which this service serves OpenID4VP verifier endpoints. As with the issuer URL, a protocol path suffix is typically appended (e.g. `https://verifier.example.com/oid4vp`).

### Authentication

Every non-public endpoint requires a valid OIDC JWT **Bearer** token (`Authorization: Bearer <token>`). Tokens are validated against a JWKS endpoint.

* **`OIDC_JWKS_URL`** — JWKS endpoint used to verify access tokens (e.g. a Keycloak realm's `.../protocol/openid-connect/certs`). Required unless `AUTH_DISABLED=true`.
* **`AUTH_DISABLED`** — set to `true` for local development only. Every request is then treated as an authenticated admin, so no token is needed. Never enable this in production.

### did:web hosting

The hostname for a `did:web` is supplied per request, in the required `domain` field of `POST /agent/did/web` (e.g. `issuer.example.com`, optionally with a port such as `example.com:8080`). There is no environment-variable fallback.

Hosted `did:web` documents are keyed by a generated UUID and served by this service over HTTPS at a fixed route; there is no configurable path prefix. See "did:web & Hosting" for path rules and examples.

### Other

* **`PORT`** — HTTP port (default `3000`).
* **`ALLOW_INSECURE_HTTP`** — set to `true` to allow plain HTTP URLs during local development. OpenID4VC requires HTTPS in production, so leave this unset (or `false`) outside of local testing.
* **`NODE_ENV`** — when set to `production`, unhandled error messages returned to clients are replaced with a generic `Internal server error`; the real cause is only written to the logs.

### Example

```
PORT=3000

# Datastore
DATABASE_URL=postgres://postgres:postgres@localhost:5432/ssi

# Public base URLs (a protocol path suffix is typically appended)
ISSUER_BASE_URL=https://issuer.example.com/oid4vci
VERIFIER_BASE_URL=https://verifier.example.com/oid4vp

# Authentication (JWKS endpoint for OIDC Bearer tokens)
OIDC_JWKS_URL=https://auth.example.com/realms/empe/protocol/openid-connect/certs
# Local-only shortcut: skip token validation, treat every caller as admin
# AUTH_DISABLED=true

# Local development over plain HTTP (never enable in production)
# ALLOW_INSECURE_HTTP=true
```

## Install & Run

```
yarn install
yarn start:dev
# or
yarn build && yarn start:prod
```

Swagger UI: `http://localhost:<PORT>/api-docs` OpenAPI JSON: `http://localhost:<PORT>/api-docs-json`

A global rate limit of 60 requests per 60 seconds applies (serving of DID documents is exempt).

## Database

* The datastore is **PostgreSQL**, configured through a single `DATABASE_URL` connection string.
* Database migrations run automatically at startup; no separate flag or command is required.
* Stored data includes hosted DID documents, tenant signing keys, credential schemas, issuance and verification session state, and credentials held by cloud wallets.

## did:web Hosting Notes

* This service creates **`did:web`** and **`did:key`** (Ed25519) identifiers and serves the `did:web` documents itself.
* A hosted `did:web` document is served at `GET /:uuid/did.json` (and at `GET /:uuid/.well-known/did.json`), where `:uuid` is the identifier generated when the DID is created.
* The `did:web` identifier is built from the `domain` supplied in the `POST /agent/did/web` request body plus a generated UUID, producing a DID of the form `did:web:issuer.example.com:<uuid>`.
* See "did:web & Hosting" for path rules and examples.

## Requirements

The wallet server runs as part of the EVDI **SSI Core** stack and shares its runtime requirements: **Node.js 20** and **PostgreSQL 16**.


# DID Document Management

The service can create Decentralized Identifiers (DIDs) and serve their DID documents. Two DID methods are created: **`did:key`** (a deterministic Ed25519 key-based DID) and **`did:web`** (a DID whose document is hosted by the service itself over HTTPS). A `did:web` document is stored server-side and published at a stable, UUID-keyed URL so that other parties can resolve it.

DIDs are used in two places:

* **Platform identities** for issuers and verifiers, created via the admin DID endpoints under `/agent`.
* **Holder (cloud-wallet) identities**, which are provisioned automatically when a holder is created.

All endpoints below require a valid OIDC JWT Bearer token (`Authorization: Bearer <token>`); the `/agent/*` routes additionally require the `admin` role. In development you can bypass auth with `AUTH_DISABLED=true`. The only exception is the public DID-document serving endpoint, which needs no auth.

## Create a did:key

Creates a deterministic Ed25519 key-based DID. No body is required.

**POST** `/agent/did/key`

### Response Body

* **did**: the generated DID, e.g. `did:key:z6Mk...`.
* **verificationMethodIds**: verification method identifiers usable for signing and assertions.

```
curl -X POST http://localhost:3000/agent/did/key \
  -H 'Authorization: Bearer <token>'
```

```json
{
  "did": "did:key:z6MkfWg...abc",
  "verificationMethodIds": ["did:key:z6MkfWg...abc#z6MkfWg...abc"]
}
```

## Create a did:web

Creates a DID whose document is hosted by the service. The DID is built as `did:web:<domain>:<uuid>`, where `<uuid>` is generated by the service and used as the public document path.

**POST** `/agent/did/web`

### Request Body

* **domain**: the hostname for the DID, without a scheme (e.g. `issuer.example.com`, optionally with a port such as `example.com:8080`). Required.

### Response Body

* **did**: the generated DID, e.g. `did:web:issuer.example.com:9b3c...`.
* **didDocument**: the full DID document JSON, including an Ed25519 verification method.

```
curl -X POST http://localhost:3000/agent/did/web \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{"domain":"issuer.example.com"}'
```

```json
{
  "did": "did:web:issuer.example.com:9b3c1d2e-...",
  "didDocument": {
    "@context": [
      "https://www.w3.org/ns/did/v1",
      "https://w3id.org/security/suites/ed25519-2018/v1"
    ],
    "id": "did:web:issuer.example.com:9b3c1d2e-...",
    "verificationMethod": [
      {
        "id": "did:web:issuer.example.com:9b3c1d2e-...#key-1",
        "type": "Ed25519VerificationKey2018",
        "controller": "did:web:issuer.example.com:9b3c1d2e-...",
        "publicKeyBase58": "..."
      }
    ],
    "authentication": ["did:web:issuer.example.com:9b3c1d2e-...#key-1"],
    "assertionMethod": ["did:web:issuer.example.com:9b3c1d2e-...#key-1"]
  }
}
```

## Resolve a DID

Returns the DID document for a DID created through this service (`did:web` or `did:key`). If the DID was not created here, the endpoint returns `200` with a `null` body — it does not resolve external DIDs.

**GET** `/agent/did/:did`

```
curl http://localhost:3000/agent/did/did:key:z6MkfWg...abc \
  -H 'Authorization: Bearer <token>'
```

## List created DIDs

Returns the DIDs created through the service.

**GET** `/agent/dids`

```
curl http://localhost:3000/agent/dids \
  -H 'Authorization: Bearer <token>'
```

## Holder DIDs

A holder is a per-user cloud-wallet tenant. Its DID is provisioned when the holder is created, so there is no separate DID-generation step for holders. Choose the method at creation time.

**POST** `/agent/holder`

### Request Body

* **method**: `key` or `web`. Determines the DID method for the holder.
* **domain**: required when `method` is `web` (hostname, e.g. `example.com`).
* **label**: optional human-readable label for the wallet.

### Response Body

* **holderId**: internal holder identifier (UUID) used in wallet API calls.
* **did**: the holder's DID.
* **cryptographicBindingMethodsSupported**, **credentialSigningAlgValuesSupported**: capabilities of the holder wallet.
* **didDocument**: included only when `method` is `web`.

```
curl -X POST http://localhost:3000/agent/holder \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{"method":"web","domain":"example.com","label":"User Wallet 1"}'
```

```json
{
  "holderId": "594b464b-20bd-495a-9fb6-87328c759629",
  "did": "did:web:example.com:594b464b-...",
  "cryptographicBindingMethodsSupported": ["did:key", "did:jwk", "did:web"],
  "credentialSigningAlgValuesSupported": ["EdDSA", "ES256"],
  "didDocument": { ... }
}
```

## Serve a hosted did:web document (public)

Every `did:web` created by the service (for an issuer, verifier, or holder) is published at a public, UUID-keyed path that matches the `<uuid>` segment of the DID. This endpoint requires no authentication and is exempt from rate limiting.

**GET** `/:uuid/did.json` **GET** `/:uuid/.well-known/did.json`

```
curl http://issuer.example.com/9b3c1d2e-.../did.json
```

If no document exists for the given UUID, the service returns `404` with the standard error body `{ statusCode, message, path, timestamp }`.

## Notes

* `did:web` documents are keyed by a service-generated UUID, not by any user identifier. The UUID is both the last segment of the DID and the document's public path.
* `did:key` and `did:web` verification methods use Ed25519 (`EdDSA`).
* The hostname for a `did:web` comes from the `domain` field of the create request, not from an environment variable (see [Environment & Setup](/develop/wallet-server/environment-and-setup) for the unused `DID_WEB_DOMAIN`/`DID_WEB_PATH` variables).
* `did:web` resolution requires HTTPS in production; for local testing only, `ALLOW_INSECURE_HTTP=true` permits plain HTTP.


# Credential Management

The Wallet Server runs a multi-tenant cloud wallet. Each **holder** is a per-user tenant that stores the Verifiable Credentials (VCs) belonging to one user. A holder acquires a credential by **processing a credential-offer URI**: the platform requests, binds, and stores the credential automatically. (This offer flow is the only way credentials enter a wallet — there is no raw-JSON upload.) Once stored, you can list the credentials in their raw (encoded) form or as decoded payloads.

All endpoints in this section are scoped to a single holder under `/holders/:holderId` and require a valid Bearer token with the **admin** role. Ownership is enforced, so a user can only operate on holders they own.

```
Authorization: Bearer <token>
```

A holder is created beforehand via `POST /agent/holder` (see [Holders and Tenancy](/develop/wallet-server/holders-and-tenancy)). The `holderId` returned there is the tenant id used in every path below.

## Acquiring a credential

**POST** `/holders/:holderId/process`

Processes a URI scanned from a QR code or received from an issuer or verifier. The same endpoint handles both directions, dispatching on the URI scheme:

* `openid-credential-offer://...` — accepts a credential offer. The holder resolves the offer, requests the offered credential(s), binds them to the holder's DID, and stores them in the wallet.
* `openid4vp://...` (or an `https://` authorization request) — responds to a verification request by selecting matching credentials and sending the presentation.

### Request Body

* **uri** (string, required): the credential-offer or authorization-request URI.

```
curl -X POST http://localhost:3000/holders/594b464b-20bd-495a-9fb6-87328c759629/process \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{"uri": "openid-credential-offer://?credential_offer_uri=..."}'
```

### Response Body

For a credential offer (issuance):

```
{
  "status": "success",
  "claimed": 1,
  "ids": ["8f2c1d4e-6a7b-4c9d-90ef-12ab34cd56ef"]
}
```

* **status**: `success` on completion.
* **claimed**: number of credentials stored from the offer.
* **ids**: identifiers of the stored credentials, usable to correlate entries returned by the listing endpoints.

For an authorization request (verification) the response is `{ "status": "success", "action": "presentation_sent" }`. For the full issuance and presentation walkthroughs, see [Processing Credential Offers and Presentation Requests](/develop/wallet-server/flows-claim-and-presentation).

## Listing raw credentials

**GET** `/holders/:holderId/credentials`

Returns every credential currently stored for the holder in its raw, encoded form (the compact string the wallet received and can present).

```
curl -H 'Authorization: Bearer <token>' \
  http://localhost:3000/holders/594b464b-20bd-495a-9fb6-87328c759629/credentials
```

### Response Body

An array of credential entries:

```
[
  {
    "id": "8f2c1d4e-6a7b-4c9d-90ef-12ab34cd56ef",
    "format": "dc+sd-jwt",
    "credential": "eyJhbGciOi...~WyJzYWx0Ii...~"
  }
]
```

* **id**: the wallet record id for the credential.
* **format**: the stored credential format (see Supported formats below).
* **credential**: the encoded credential — a compact SD-JWT or JWT string, or a JSON object for JSON-LD VCs.

## Listing decoded credentials

**GET** `/holders/:holderId/credentials/decoded`

Returns the same credentials with their payloads decoded, which is convenient for inspecting claims without parsing the compact encoding yourself.

```
curl -H 'Authorization: Bearer <token>' \
  http://localhost:3000/holders/594b464b-20bd-495a-9fb6-87328c759629/credentials/decoded
```

### Response Body

An array of decoded entries:

```
[
  {
    "id": "8f2c1d4e-6a7b-4c9d-90ef-12ab34cd56ef",
    "format": "dc+sd-jwt",
    "decoded": {
      "header": { "alg": "EdDSA", "typ": "dc+sd-jwt" },
      "payload": { "vct": "https://issuer.example.com/vct/employee-badge", "_sd": ["..."] },
      "prettyClaims": {
        "vct": "https://issuer.example.com/vct/employee-badge",
        "employee_id": "E-1024",
        "full_name": "Ada Lovelace",
        "department": "Engineering"
      }
    }
  }
]
```

* **id**: the wallet record id.
* **format**: the decoded credential format.
* **decoded**: the decoded content, which varies by format:
  * **SD-JWT VC** — `header`, `payload`, and `prettyClaims` (claims with selective-disclosure digests resolved into their values); `kbJwt` is included when a key-binding JWT is present.
  * **JWT-VC-JSON** — `header` and `payload`.
  * For a JSON-LD VC stored as a JSON object, `decoded` is the object itself.
* **error** (optional): a message describing why an entry could not be decoded; `decoded` is `null` in that case.

## Supported formats

The cloud wallet can store and list credentials in the formats the platform issues, plus additional formats it may receive during presentation flows:

* **SD-JWT VC** — reported as `dc+sd-jwt`.
* **JWT-VC-JSON** — reported as `jwt_vc_json`.
* **JSON-LD VC** — reported as `ldp_vc`.
* **W3C v2 credentials** — reported with their claim format (`vc+jwt` or `vc+sd-jwt`).
* **mdoc** — reported as `mso_mdoc`.

## Deletion

The holder API surface covers processing URIs and listing credentials (`POST /holders/:holderId/process`, `GET /holders/:holderId/credentials`, and `GET /holders/:holderId/credentials/decoded`). Deleting individual credentials or clearing a holder's store is not currently part of the API.


# Flows: Claim and Presentation

A cloud wallet (a **holder**) runs two end-to-end flows: **claiming** a credential from an issuer (OpenID4VCI) and **presenting** credentials to a verifier (OpenID4VP). Both start the same way — you hand the wallet the URI a mobile wallet would scan from a QR code — and both complete in a single call to **`POST /holders/:holderId/process`**, which dispatches on the URI scheme.

This page walks through what actually happens inside each flow, step by step. For the endpoint's request/response reference, options, and error catalog, see [Processing URIs](/develop/wallet-server/processing-uris). A holder is created beforehand via `POST /agent/holder`; see [Holders and Tenancy](/develop/wallet-server/holders-and-tenancy).

## The Claim Flow (Issuance)

An issuer that wants to hand out a credential creates a **credential offer** and exposes it as an `openid-credential-offer://...` URI (usually rendered as a QR code or deep link). Claiming it into a cloud wallet runs like this:

1. **Submit the URI.** You call `POST /holders/:holderId/process` with the offer URI. The `openid-credential-offer://` scheme routes the call into the issuance flow.
2. **DID check.** The wallet verifies the holder has a DID — the identity the credential will be bound to. It uses the holder's first DID and picks a verification method from its DID document. A holder without a DID cannot claim, and the call fails with `400 Bad Request`.
3. **Resolve the offer.** The wallet fetches the offer from the issuer and reads which credential configurations are being offered.
4. **Request an access token.** Using the offer's pre-authorized code, the wallet obtains an access token from the issuer.
5. **Request the credentials.** The wallet requests every offered credential configuration, proving control of the holder's DID so the issuer binds each credential to it.
6. **Store.** Each returned credential is stored in the wallet's store for its format — SD-JWT VC, JWT-VC-JSON, JSON-LD VC, W3C v2 (`vc+jwt` / `vc+sd-jwt`), or mdoc.

The whole sequence runs synchronously inside the one call. When it returns, the flow is over: the response reports how many credentials were stored and their record ids (`{ "status": "success", "claimed": 1, "ids": ["..."] }`), and on the issuer's side the [issuance session](/develop/issuer/issuance-sessions) moves to `Completed`. The new credentials are immediately visible through the listing endpoints — see [Credential Management](/develop/wallet-server/credential-management).

## The Presentation Flow (Verification)

A verifier that wants proof of something creates an **authorization request** and exposes it as an `openid4vp://...` (or `https://...`) URI. Answering it from a cloud wallet runs like this:

1. **Submit the URI.** You call the same `POST /holders/:holderId/process` endpoint with the request URI. The `openid4vp://` or `https://` scheme routes the call into the presentation flow.
2. **Resolve the request.** The wallet fetches the verifier's authorization request and reads its credential query — either **DIF Presentation Exchange v2** or **DCQL**, whichever the request carries.
3. **Select credentials.** The wallet matches the query against the holder's stored credentials and selects the ones that satisfy it. Selection is fully automatic; the caller never names a credential. If nothing in the wallet matches, the presentation cannot be built and the call fails.
4. **Build and submit.** The wallet builds the presentation (honoring selective disclosure for SD-JWT credentials) and submits it to the verifier's response endpoint.

Again, everything happens inside the single call. The response `{ "status": "success", "action": "presentation_sent" }` means the presentation was delivered; the verifier's verification session then records the result on the verifier's side.

## States and Sessions

The wallet side is stateless by design: there is no wallet-side session or flow object to poll, and no initialize/confirm handshake — each `process` call runs its flow to completion and returns the final result. Protocol state lives with the counterparties: the issuer tracks an issuance session (from offer creation to `Completed`) and the verifier tracks a verification session for its request. Access control is per holder — a caller can only run these flows for holders they own.

## What Can Go Wrong

* **Claiming without a DID** fails at step 2 with `400 Bad Request` — the holder's DID is created with the holder, so this only happens for misconfigured tenants.
* **Presenting without a matching credential** fails at step 3 — claim the required credential first, then retry.
* **An unrecognized URI scheme** is rejected with `400 Bad Request` before either flow starts.

For the exact error messages, response bodies, and curl examples, see [Processing URIs](/develop/wallet-server/processing-uris).


# Processing URIs

A cloud wallet (a **holder**) interacts with issuers and verifiers by **processing a URI** — the same `openid-credential-offer://`, `openid4vp://`, or `https://` content that a mobile wallet would obtain by scanning a QR code or following a deep link. A single endpoint handles both directions: claiming offered credentials from an issuer (**OpenID4VCI**) and responding to a presentation request from a verifier (**OpenID4VP**). The wallet inspects the URI scheme and runs the matching protocol automatically, so the caller never has to know in advance whether a given QR code is an offer or a request.

This page is the endpoint reference — request and response bodies, options, and errors. For a step-by-step walkthrough of what happens inside each flow, see [Processing Credential Offers and Presentation Requests](/develop/wallet-server/flows-claim-and-presentation).

All routes in this section are scoped to one holder under `/holders/:holderId` and require a valid Bearer token with the **admin** role. Ownership is enforced, so a user can only process URIs for holders they own.

A holder is created beforehand via `POST /agent/holder` (see [Holders and Tenancy](/develop/wallet-server/holders-and-tenancy)). The `holderId` returned there is the tenant id used in the path below.

## Processing a URI

**POST** `/holders/:holderId/process`

Accepts a URI scanned from a QR code, copied from a deep link, or received directly from an issuer or verifier. The endpoint dispatches on the URI scheme:

* `openid-credential-offer://...` — accepts a **credential offer**. The wallet resolves the offer, requests an access token, requests the offered credential(s), binds them to the holder's DID, and stores them.
* `openid4vp://...` or `https://...` — responds to an **authorization request**. The wallet resolves the request, selects matching credentials, builds the presentation, and submits it.

Any other scheme returns `400 Bad Request` with the message `Unknown URI protocol. Expected openid-credential-offer:// or openid4vp://`.

### Request Body

* **uri** (string, required): the URI to process. Supports both `openid-credential-offer://...` credential offers and `openid4vp://...` (or `https://...`) authorization requests — the same content a mobile wallet would scan.

```
curl -X POST http://localhost:3000/holders/594b464b-20bd-495a-9fb6-87328c759629/process \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{"uri": "openid-credential-offer://?credential_offer_uri=https://issuer.example.com/offers/abc123"}'
```

### Response Body — Credential Offer

When the URI is a credential offer, the wallet claims the offered credential(s) and returns:

```
{
  "status": "success",
  "claimed": 1,
  "ids": ["8f2c1d4e-6a7b-4c9d-90ef-12ab34cd56ef"]
}
```

* **status**: `success` on completion.
* **claimed**: number of credentials stored from the offer.
* **ids**: wallet record identifiers of the newly stored credentials, usable to correlate entries returned by the credential listing endpoints.

For example, processing an `EmployeeBadge@1.0:sd-jwt` offer stores one SD-JWT VC, and the decoded listing then shows its `vct` and selectively disclosable claims such as `employee_id`, `full_name`, and `department`.

### Response Body — Authorization Request

When the URI is an authorization request, the wallet selects the credentials that satisfy the verifier's query and sends the presentation. Selection is automatic — the caller never supplies a credential — and uses **DIF Presentation Exchange v2** or **DCQL**, depending on the query carried by the request. The response is:

```
{
  "status": "success",
  "action": "presentation_sent"
}
```

* **status**: `success` on completion.
* **action**: `presentation_sent`.

```
curl -X POST http://localhost:3000/holders/594b464b-20bd-495a-9fb6-87328c759629/process \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{"uri": "openid4vp://?request_uri=https://verifier.example.com/requests/xyz789"}'
```

## Prerequisites and Errors

* **Credential offers** require the holder to have at least one DID, used to bind the issued credential. If the holder has no DID, the request fails with `400 Bad Request` (`Holder has no DIDs created. Cannot accept credential offer.`). A DID is created when the holder is registered via `POST /agent/holder`.
* **Authorization requests** require the holder to already hold a credential that satisfies the verifier's query. If nothing in the wallet matches the Presentation Exchange definition or DCQL query, the presentation cannot be built.
* An unrecognized URI scheme returns `400 Bad Request` (`Unknown URI protocol. Expected openid-credential-offer:// or openid4vp://`).

## Notes

* The endpoint runs the full protocol synchronously and returns the final result. There is no separate initialize/confirm step and no per-call flow lifecycle to track.
* Isolation between callers comes from holder ownership on the route, not from a per-flow session — only the owner of a holder can process URIs for it.
* The same URI content that this endpoint consumes is exactly what an end user's mobile wallet would scan. Issuers expose offer URIs and PNG QR codes from their credential-offer endpoints, and verifiers expose authorization-request URIs and PNG QR codes from their authorization-request endpoints.
* After processing an offer, inspect what the holder now holds with `GET /holders/:holderId/credentials` (raw) and `GET /holders/:holderId/credentials/decoded` (decoded). See [Credential Management](/develop/wallet-server/credential-management).


# did:web & Hosting

The Wallet Server creates `did:web` and `did:key` identifiers and serves the resulting `did:web` documents itself over HTTPS. Use `did:web` when you control a domain and want the DID document resolvable at a public URL; use `did:key` when you do not control a domain and want a self-contained identifier with no hosting.

`did:web` documents are stored server-side and published at a public, unauthenticated, throttle-exempt endpoint. The signing keys behind each document are Ed25519.

## How the DID and its URL are built

When you create a `did:web`, you supply the **domain** that hosts it. The server generates a UUID, builds the identifier as `did:web:<domain>:<uuid>`, creates a fresh Ed25519 key that it embeds as the document's verification method (`#key-1`), and stores the resulting document keyed by that UUID.

Following the `did:web` specification, each `:` after the method maps to a `/` in the resolution URL. A bare-domain DID (`did:web:example.com`) would resolve at `/.well-known/did.json`, but because these DIDs carry a path segment (the UUID), resolvers fetch the document at `<path>/did.json`. So `did:web:example.com:<uuid>` resolves to:

```
https://example.com/<uuid>/did.json
```

A non-default port, if any, travels as part of the domain value (for example `example.com:8080`), not as a separate setting.

### Configuration

The effective domain for any given DID always comes from the `domain` field in the create request; there is no operator-level default. The server builds the identifier directly as `did:web:<domain>:<uuid>` from that value, with no separate base path inserted. (See [Environment & Setup](/develop/wallet-server/environment-and-setup) for the unused `DID_WEB_DOMAIN`/`DID_WEB_PATH` variables.)

`did:web` requires HTTPS in production; for local development only, set `ALLOW_INSECURE_HTTP=true` to permit `http://`.

## Serving the document

**`GET /:uuid/did.json`** — public, no authentication, exempt from rate limiting. This is the URL a spec-compliant `did:web` resolver fetches for a path-bearing DID.

The Wallet Server also serves the same document at the additional alias **`GET /:uuid/.well-known/did.json`**.

```bash
curl https://example.com/<uuid>/did.json
```

### Response Body

The stored DID document, for example:

```json
{
  "@context": [
    "https://www.w3.org/ns/did/v1",
    "https://w3id.org/security/suites/ed25519-2018/v1"
  ],
  "id": "did:web:example.com:<uuid>",
  "verificationMethod": [
    {
      "id": "did:web:example.com:<uuid>#key-1",
      "type": "Ed25519VerificationKey2018",
      "controller": "did:web:example.com:<uuid>",
      "publicKeyBase58": "..."
    }
  ],
  "authentication": ["did:web:example.com:<uuid>#key-1"],
  "assertionMethod": ["did:web:example.com:<uuid>#key-1"]
}
```

Requesting an unknown UUID returns `404` with the standard error body `{ statusCode, message, path, timestamp }`.

## Creating a hosted did:web

Two routes create a `did:web`. Both require a valid Bearer token (`Authorization: Bearer <token>`) with the `admin` role; in development you may set `AUTH_DISABLED=true`.

### Platform identity

**`POST /agent/did/web`** creates a standalone hosted `did:web` (for example, to back an issuer or verifier).

#### Request Body

* **`domain`** (required) — a valid hostname, optionally with a port, e.g. `issuer.example.com` or `issuer.example.com:8080`.

```bash
curl -X POST https://<wallet-server>/agent/did/web \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{ "domain": "issuer.example.com" }'
```

#### Response Body

* **`did`** — the new identifier, e.g. `did:web:issuer.example.com:<uuid>`.
* **`didDocument`** — the generated DID document (also now served publicly).

### Per-user wallet identity

**`POST /agent/holder`** creates a per-user cloud-wallet tenant. Pass `method: "web"` with a `domain` to back the wallet with a hosted `did:web`; the document is served at the same public URL. Use `method: "key"` for a `did:key` wallet that needs no hosting.

#### Request Body

* **`method`** (required) — `"web"` or `"key"`.
* **`domain`** — required when `method` is `"web"`; ignored for `"key"`.
* **`label`** (optional) — a human-readable name for the wallet.

```bash
curl -X POST https://<wallet-server>/agent/holder \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{ "method": "web", "domain": "example.com", "label": "User Wallet 1" }'
```

#### Response Body

* **`holderId`** — the wallet tenant id.
* **`did`** — the wallet's DID, e.g. `did:web:example.com:<uuid>`.
* **`cryptographicBindingMethodsSupported`**, **`credentialSigningAlgValuesSupported`** — the wallet's binding and signing capabilities.
* **`didDocument`** — present for `did:web` holders.

## did:key as the no-hosting alternative

When you do not control a domain, create a `did:key` instead. It carries its public key inside the identifier, so it is fully self-contained and requires no hosted document or HTTPS endpoint.

**`POST /agent/did/key`** takes no body and returns a `did:key:z6Mk...` identifier with its verification method ids. The same choice is available for wallets via `POST /agent/holder` with `method: "key"`.

```bash
curl -X POST https://<wallet-server>/agent/did/key \
  -H "Authorization: Bearer <token>"
```


# Security Considerations

The service protects every non-public endpoint with OIDC Bearer-token authentication, role-based access control, per-tenant ownership checks, and global rate limiting. This page describes the security model so you can integrate against it safely.

## Authentication

Authenticated requests carry a standard OAuth 2.0 / OIDC access token in the HTTP `Authorization` header:

```bash
curl https://issuer.example.com/agent/dids \
  -H "Authorization: Bearer <access_token>"
```

* **Scheme:** `Authorization: Bearer <JWT>` (the OpenAPI document marks protected operations with the standard bearer security scheme).
* **Signing algorithm:** `RS256`.
* **Key resolution:** the token's `kid` header selects the public verification key, which is fetched from the OIDC provider's JWKS endpoint configured in `OIDC_JWKS_URL`. Keys are cached for 10 minutes, and JWKS lookups are themselves rate-limited (10 requests per minute) to protect the provider.
* **Expiry:** expired tokens are rejected; expiration is always enforced.

Obtain tokens from your OIDC provider (for example, a Keycloak realm). The service does not mint tokens and exposes no token-signing or debug-key endpoints.

## Authorization

Holding a valid token is not always sufficient — some routes additionally require a role. The service reads roles from the token's `realm_access.roles` array (client roles in `resource_access` are also honored).

* The **`admin`** role is required on the agent/DID, issuer-registration, verifier-registration, and holder/wallet management routes (`/agent/*` and `/holders/:holderId/*`).
* The remaining issuer routes (schemas, credential offers, issuance sessions, direct signing) and verifier routes (authorization requests, verification sessions, `/credentials/verify`) require a valid token but no specific role.
* Public routes — the root and version endpoints, and the hosted `did:web` documents — require no token at all.

A request with a valid token but an insufficient role is rejected with `403 Forbidden`.

### Development bypass

For local development, set `AUTH_DISABLED=true`. Every request is then treated as an authenticated `admin` principal and no token is required. **Never enable this outside local development.**

## Multi-Tenant Isolation

Each cloud-wallet **holder** is a per-user tenant created via `POST /agent/holder` and owned by the OIDC user (identified by the token's `sub`) who created it.

* An ownership guard enforces that a caller can only read or operate holders they own. Accessing another user's holder — listing its credentials, decoding them, or processing an offer/presentation URI through it — returns `403 Forbidden`.
* `GET /agent/holders` returns only the holders owned by the authenticated user.

This means the wallet tenant boundary is the `holderId` together with its owning user, not the raw token subject alone.

## Transport & Keys

* Always run the service behind **HTTPS** in production. OpenID4VCI and OpenID4VP flows require HTTPS, and insecure HTTP URLs are rejected by default.
* `ALLOW_INSECURE_HTTP=true` relaxes this for local development only. Do not set it in production.
* Protect your `.env` files, database credentials, and the OIDC provider configuration.
* Prefer short token TTLs, especially for automated/machine clients, and rotate provider keys periodically.
* The service does not implement credential revocation or status lists, so a verified credential is not checked against any revocation source — design your trust model around credential expiry and re-issuance instead.

## Rate Limiting

A global limit of **60 requests per 60 seconds** applies to the API. Serving of hosted `did:web` documents is exempt so that DID resolution by external parties is never throttled.


# Error Handling and Troubleshooting

The service returns errors through a single global exception filter, so every error response has the same shape regardless of which endpoint produced it. This page describes that shape, then catalogs the errors you are most likely to encounter, organized by HTTP status code, and finishes with startup and connectivity issues.

## Error response shape

All errors are returned as JSON with a consistent envelope:

```json
{
  "statusCode": 403,
  "message": "Insufficient role",
  "path": "/agent/dids",
  "timestamp": "2026-06-22T10:15:30.123Z"
}
```

* **statusCode** — the HTTP status code, also reflected in the response status line.
* **message** — a human-readable description; for validation errors this may be an array of messages.
* **path** — the request path that produced the error.
* **timestamp** — ISO-8601 time the error was generated.

Unhandled server errors are returned as `500` with a generic `Internal server error` message in production; details are not leaked to the client.

## 400 Bad Request

Returned when the request body, query, or URI is malformed or semantically invalid.

* **Validation failure** — a required field is missing or has the wrong type. For example, creating a schema without `name`/`version`, or a credential offer with an empty `credentialConfigurationIds` array. The `message` lists the specific fields that failed.
* **Unknown URI protocol** (`POST /holders/:holderId/process`) — the `uri` you submitted is neither a credential offer nor an authorization request. The message is `Unknown URI protocol. Expected openid-credential-offer:// or openid4vp://`. Pass the full `openid-credential-offer://...` URI returned by a credential offer, or the `openid4vp://...` (or `https://...`) authorization request URI from a verifier.
* **Holder has no DIDs** (`POST /holders/:holderId/process`) — the wallet tenant has no DID with which to bind a claimed credential. The message is `Holder has no DIDs created. Cannot accept credential offer.` Create the holder with a DID method (`POST /agent/holder` with `{ method: "key" }` or `{ method: "web" }`) before claiming offers.
* **Unknown property in the body** — the global validation pipe whitelists request properties, so a JSON body containing a property that is not part of the endpoint's schema is rejected with a message like `property X should not exist`.

## 401 Unauthorized

Returned by authentication, before any role check, when the Bearer token cannot be validated. Tokens must be `RS256`-signed and presented as `Authorization: Bearer <JWT>`.

* **Missing token** — no `Authorization: Bearer <JWT>` header is sent; the request is rejected with a generic `401 Unauthorized`.
* **Malformed token** — a token is present but is not a well-formed JWT (it cannot be decoded). Message: `Invalid access token`.
* **Missing `kid`** — the token header has no `kid`, so the signing key cannot be selected. Message: `Access token header is missing kid`. Ensure your OIDC provider includes a `kid` in the JWT header (Keycloak does this by default).
* **Signing key not found in JWKS** — the token's `kid` does not match any key published at `OIDC_JWKS_URL`. This happens after a provider key rotation (wait for the 10-minute key cache to refresh) or when the token was issued by a different provider/realm than the one configured.
* **Expired token** — expiration is always enforced. Request a fresh token from your OIDC provider.
* **`OIDC_JWKS_URL` misconfigured** — the JWKS endpoint is unreachable or returns no usable keys, so no token can be verified. Confirm the URL points at your provider's JWKS document (for example `https://keycloak.example.com/realms/<realm>/protocol/openid-connect/certs`).

For local development you can bypass authentication entirely with `AUTH_DISABLED=true`, which treats every request as an authenticated `admin` principal. Never enable this outside local development.

## 403 Forbidden

Returned when the token is valid but the caller is not allowed to perform the operation.

* **Insufficient role** — the route requires the `admin` role and the token's `realm_access.roles` (or client roles in `resource_access`) does not include it. The `admin` role is enforced on the `/agent/*` (DID, issuer, verifier, holder management) and `/holders/:holderId/*` (wallet) routes. Message: `Insufficient role`.
* **Holder not owned by the caller** (`/holders/:holderId/*`) — the ownership guard denies access to any `holderId` the caller did not create. Message: `Access to holder denied`. A `holderId` that does not exist at all is indistinguishable from one owned by another user — both return this `403`. A user can only read, decode, or process URIs through holders they created; list your own with `GET /agent/holders`.

## 404 Not Found

Returned when the addressed resource does not exist.

* **Unknown schema id** — message: `Schema not found.` Confirm the id returned when the schema was created.
* **Unknown hosted `did:web` document** (`GET /:uuid/did.json` or `GET /:uuid/.well-known/did.json`) — message: `DID Document not found`.

Two related lookups behave differently: `GET /agent/did/:did` returns `200` with a `null` body for a DID that was not created by this service, and unknown issuer, verifier, or session ids currently surface as `500` errors rather than `404`.

## 429 Too Many Requests

A global limit of **60 requests per 60 seconds** applies to the API. Exceeding it returns `429`. Throttle or batch your calls and retry after a short delay. Serving of hosted `did:web` documents is exempt, so external DID resolution is never rate-limited.

## 500 Internal Server Error

Errors raised outside the HTTP layer — including protocol failures inside a wallet flow — are converted to `500` by the global exception filter. When `NODE_ENV` is `production`, the `message` is replaced with a generic `Internal server error`; otherwise the underlying error message is returned. In production, check the server logs for the real cause.

* **No matching credential for a presentation** (`POST /holders/:holderId/process`) — the holder wallet has no stored credential that satisfies the verifier's DCQL or Presentation Exchange query. Outside production the message is the underlying error, for example `Could not find the required credentials for the presentation submission`. Claim the required credential (for example `EmployeeBadge@1.0:sd-jwt`) into the wallet first, then retry the authorization request.
* **Upstream issuer/verifier rejection** — while processing an offer or authorization request, the remote issuer or verifier returned an error (expired or already-consumed offer, wrong PIN, rejected presentation). The upstream reason appears in `message` outside production; in production it is only in the logs.
* **Unknown issuer, verifier, or session id** — looking up an id the service does not know currently surfaces as `500` rather than `404`.

## Startup & configuration errors

The service validates its configuration at boot and refuses to start when required variables are missing.

* **Missing required env vars** — `ISSUER_BASE_URL`, `VERIFIER_BASE_URL`, and `DATABASE_URL` are mandatory. If any is absent the process exits with `Invalid environment variables:` followed by the offending variable, for example `- ISSUER_BASE_URL must be defined`.
* **Missing JWKS configuration** — unless `AUTH_DISABLED=true`, `OIDC_JWKS_URL` must be set, otherwise startup fails with `OIDC configuration missing. Set OIDC_JWKS_URL.`
* **Database connectivity** — connection failures come from `DATABASE_URL`. Verify the host, port, credentials, and database name, and that the database is reachable from the service and migrations have run.
* **Insecure HTTP blocked** — OpenID4VCI and OpenID4VP flows require HTTPS, so insecure HTTP base URLs are rejected by default. For local development only, set `ALLOW_INSECURE_HTTP=true`. Never set it in production.

## Troubleshooting tips

* **Is the service up?** Call `GET /` for a hello string or `GET /version` for `{ "name": "ssi-core", "version": "..." }`. Neither requires authentication.
* **Authentication problems?** Obtain a valid `admin` token from your OIDC provider and send it as `Authorization: Bearer <token>`. Locally, set `AUTH_DISABLED=true` to skip token handling while you debug other behavior.
* **Inspect the contract.** The interactive API explorer at `GET /api-docs` and the machine-readable spec at `GET /api-docs-json` show every endpoint, request body, and response schema.
* **Read the envelope.** The `path` and `message` fields identify exactly which request failed and why; for validation errors the `message` array names each invalid field.


# Health & Ops

This page covers the endpoints and operational behavior you need to run the service in production: liveness/version probes, automatic database migrations, persisted data, rate limiting, and logging.

## Liveness & Version Probes

The service exposes two public, unauthenticated endpoints suitable for uptime checks and deployment verification.

**`GET /`**

Returns a plain string (`Hello World!`). Use it as a lightweight liveness probe.

```
curl https://wallet.example.com/
```

**`GET /version`**

Returns the service name and the running build version. Use it to confirm which release is deployed.

```
curl https://wallet.example.com/version
```

Response Body:

```json
{ "name": "ssi-core", "version": "<build version>" }
```

The `version` value is read from the build's `package.json`, so it reflects whatever release is deployed.

To confirm database connectivity, call an admin-authenticated read such as **`GET /agent/dids`** (an `/agent/*` route requiring an admin Bearer token): a `200` indicates the service and its database are reachable. The service has no dedicated readiness endpoint.

## Migrations & Persistence

Database migrations run **automatically at startup** against the PostgreSQL instance referenced by `DATABASE_URL`. Migrations are applied before the service begins serving traffic, so deployment needs no separate migration step. Point a fresh service at an empty database and it creates its own tables on first boot.

Persisted data includes:

* Hosted `did:web` documents served by the service over HTTPS
* Signing keys managed by the service's key store
* Credential schemas and their versions
* Authenticated users and their holder (cloud-wallet tenant) mappings
* Issued and stored credentials, plus issuance and verification session records

All persisted state is local to the service: `did:web` documents are created and served by the service itself, and `did:key` documents are self-contained (they resolve from the key material alone) — neither has any on-chain record.

## Rate Limiting

A global rate limit applies to the API: **60 requests per 60 seconds** per client. Requests beyond the limit receive `429 Too Many Requests`. Account for this when scripting bulk issuance or running automated tests.

Public DID document serving is **exempt** from the rate limit, so wallets and verifiers resolving `did:web` documents are never throttled:

* **`GET /:uuid/did.json`**
* **`GET /:uuid/.well-known/did.json`**

## Observability

* Application logs cover the issuance and verification request lifecycle (offer creation, session state changes, signing, and presentation handling), plus migration progress at startup.
* Errors are returned in a consistent envelope so failures are easy to correlate with logs:

```json
{
  "statusCode": 404,
  "message": "DID Document not found",
  "path": "/abc123/.well-known/did.json",
  "timestamp": "2026-06-22T10:15:30.000Z"
}
```

For interactive inspection of the full API, the service serves Swagger UI at **`GET /api-docs`** and the raw OpenAPI document at **`GET /api-docs-json`**.


# MCP/AI Integration

The Wallet Server's holder API is a good fit for AI agents that need to act as a credential holder. Every operation is a plain, synchronous HTTP call, so it can be exposed as tools in a Model Context Protocol (MCP) server or any other agent framework:

* **Create a holder wallet** — `POST /agent/holder` (choose `did:key` or `did:web`)
* **Process a URI** — `POST /holders/:holderId/process` claims a credential offer or answers a presentation request from QR-code content
* **List stored credentials** — `GET /holders/:holderId/credentials` and `GET /holders/:holderId/credentials/decoded`

## Typical Agent Tasks

* "Create a holder wallet and tell me its DID."
* "Process this credential-offer URI and confirm the credential was stored."
* "Scan the verifier QR and present the matching credential."

## Setup Notes for Agents

* Authenticate with `Authorization: Bearer <JWT>`; the token's `sub` identifies the agent/user and scopes holder ownership
* Keep tokens short-lived; refresh as needed
* Point agents to Swagger UI at `/api-docs` or the OpenAPI JSON at `/api-docs-json` to discover operations

## Local Development

For local development set `AUTH_DISABLED=true`; every request is then treated as an authenticated admin, so no token is needed. Never enable this outside local development.


# References and Standards

The Wallet Server is built on open, interoperable standards so that the credentials it issues and the presentations it verifies work with conformant third-party wallets and verifiers. The specifications below underpin its data models, identifiers, issuance and presentation protocols, credential formats, and authentication.

## Verifiable Credentials & Identifiers

* W3C Verifiable Credentials Data Model: <https://www.w3.org/TR/vc-data-model/>
* W3C Decentralized Identifiers (DID) Core: <https://www.w3.org/TR/did-core/>
* W3C DID Specification Registries (covers `did:web` and `did:key`): <https://www.w3.org/TR/did-spec-registries/>
* The `did:key` Method: <https://w3c-ccg.github.io/did-method-key/>
* The `did:web` Method: <https://w3c-ccg.github.io/did-method-web/>

## Issuance & Presentation Protocols

* OpenID for Verifiable Credential Issuance (OpenID4VCI) — the protocol the service speaks to issue credentials into a holder via the pre-authorized code flow: <https://openid.net/specs/openid-4-verifiable-credential-issuance-1\\_0.html>
* OpenID for Verifiable Presentations (OpenID4VP) — the protocol used to request and verify presentations from a holder: <https://openid.net/specs/openid-4-verifiable-presentations-1\\_0.html>

## Credential Formats

* SD-JWT-based Verifiable Credentials (SD-JWT VC) — the selective-disclosure credential format (identified as `sd-jwt-vc` / `vc+sd-jwt`, and reported as `dc+sd-jwt` in the holder's stored-credential listing): <https://datatracker.ietf.org/doc/draft-ietf-oauth-sd-jwt-vc/>
* Selective Disclosure for JWTs (SD-JWT) — the underlying selective-disclosure mechanism: <https://datatracker.ietf.org/doc/draft-ietf-oauth-selective-disclosure-jwt/>

## Credential Query & Matching

* DIF Presentation Exchange v2 — one of the two ways a verifier describes which credentials it needs: <https://identity.foundation/presentation-exchange/spec/v2.0.0/>
* Digital Credentials Query Language (DCQL) — the alternative credential query language, defined within OpenID4VP: <https://openid.net/specs/openid-4-verifiable-presentations-1\\_0.html#name-dcql-query>

## Authentication & Tokens

* OAuth 2.0 Authorization Framework — the basis for Bearer token authentication on the service's protected endpoints: <https://www.rfc-editor.org/rfc/rfc6749>
* OpenID Connect Core — the identity layer whose JWTs the service validates (`Authorization: Bearer <token>`): <https://openid.net/specs/openid-connect-core-1\\_0.html>
* JSON Web Token (JWT): <https://www.rfc-editor.org/rfc/rfc7519>
* JSON Web Signature (JWS): <https://www.rfc-editor.org/rfc/rfc7515>


# FAQ

Short answers to common questions about the Wallet Server. For full details, see the linked pages elsewhere in this section.

## Why use a server-side cloud wallet when mobile wallets exist?

Automation. A cloud wallet lets CI pipelines, backend scripts, and AI agents run the same OpenID4VCI issuance and OpenID4VP presentation flows that a person would run in a phone wallet — without a human scanning QR codes. The flows stay standards-compliant, so a credential claimed or presented by a server-side holder is interoperable with mobile wallets. Use a mobile wallet when an end user holds their own credentials on their device; use a server-side holder when a service or agent needs to hold and use credentials programmatically.

## How does multi-tenancy work?

Each cloud wallet is a **holder** — a per-user tenant identified by a `holderId`. A holder is created for an authenticated OIDC user via `POST /agent/holder`, and that user can list their own holders with `GET /agent/holders`. Every holder operation lives under `/holders/:holderId` and is guarded by ownership: a user can only process URIs and read credentials for holders they created. Requests to these routes require a valid Bearer token with the **admin** role.

```
Authorization: Bearer <token>
```

## How does a holder get a credential?

By processing a credential-offer URI — a single call:

**POST** `/holders/:holderId/process`

```json
{ "uri": "openid-credential-offer://?credential_offer_uri=..." }
```

The holder resolves the offer, requests the offered credential(s), binds them to the holder's DID, and stores them. The response reports how many were stored:

```json
{ "status": "success", "claimed": 1, "ids": ["<record-id>"] }
```

Credentials are never uploaded as raw JSON; they are always acquired through the offer flow.

## How does the wallet present a credential?

You pass the verifier's authorization-request URI to the **same** `/holders/:holderId/process` endpoint:

```json
{ "uri": "openid4vp://?request_uri=..." }
```

There is no separate confirmation step and you do not pass the credential yourself. The holder resolves the request, automatically selects a matching credential from its store (using DIF Presentation Exchange or DCQL, whichever the request uses), and sends the presentation:

```json
{ "status": "success", "action": "presentation_sent" }
```

The endpoint dispatches on the URI scheme, so `openid-credential-offer://` claims a credential while `openid4vp://` (or an `https://` authorization request) presents one.

## How do I list a holder's stored credentials?

Two endpoints, both scoped to one holder:

* **GET** `/holders/:holderId/credentials` — raw, encoded credentials: `[{ id, format, credential }]`.
* **GET** `/holders/:holderId/credentials/decoded` — decoded payloads: `[{ id, format, decoded, error? }]`.

Both endpoints return every credential the holder currently stores; there is no filtering.

## Which DID methods and credential formats are supported?

A holder is provisioned with a **`did:key`** (a self-contained Ed25519 identifier, no hosting required) or a **`did:web`** (whose document the service hosts over HTTPS). The supported credential formats are **SD-JWT VC** (`sd-jwt-vc` / `vc+sd-jwt`) and **JWT-VC-JSON** (`jwt_vc_json`). A holder can additionally store JSON-LD VC, W3C v2 (`vc+jwt` / `vc+sd-jwt`), and mdoc credentials it receives. (When listing stored credentials, the holder reports SD-JWT VCs with the media type `dc+sd-jwt`.)

## Can I use did:web without owning a domain?

For production you must control the domain you supply, because the DID document is published under that hostname and other parties resolve it there. The identifier is built as `did:web:<domain>:<uuid>` and served at `/<uuid>/did.json` (also at `/<uuid>/.well-known/did.json`), so the DID encodes the host you choose. The host comes strictly from the `domain` field of the holder-creation request — you must supply it, and creating a `did:web` holder without it fails. For local testing you can point a hostname at the service (for example via your hosts file or port forwarding) and pass that hostname as the `domain` field when you create the holder (`POST /agent/holder` with `{ "method": "web", "domain": "<your-host>" }`); the DID will encode that host. If you do not control any domain, create the holder with `method: "key"` instead — `did:key` needs no hosting.

## Does the service create did:empe or anchor DIDs on a blockchain?

No. The EMPE DID method (`did:empe`) and the EMPE blockchain are separate parts of the wider Empeiria ecosystem. The Wallet Server does not create or anchor `did:empe` identifiers; its holders use `did:key` and `did:web`, and the service serves their DID documents itself over HTTPS.


# API Reference


# DID Management

## Generate DID documents

> Generates new DID documents for the specified methods.

```json
{"openapi":"3.0.0","info":{"title":"Wallet API","version":"1.0"},"security":[{"oneclick-jwt":[]}],"components":{"securitySchemes":{"oneclick-jwt":{"scheme":"bearer","bearerFormat":"JWT","type":"http","description":"One-Click access token (Authorization: Bearer <JWT>)"}},"schemas":{"GenerateDidRequestDto":{"type":"object","properties":{"methods":{"description":"DID method which will be used for generate DID documents","type":"array","items":{"type":"string"}}},"required":["methods"]},"DocumentObjectDto":{"type":"object","properties":{"@context":{"type":"string","description":"JSON-LD context for the DID document"},"id":{"type":"string","description":"The DID"},"controller":{"type":"string","description":"The controller of the DID document"},"verificationMethod":{"type":"object","description":"The verification methods associated with the DID document"},"authentication":{"description":"Authentication methods for the DID document","type":"array","items":{"type":"string"}},"assertionMethod":{"description":"Assertion methods for the DID document","type":"array","items":{"type":"string"}},"capabilityInvocation":{"description":"Capability invocation methods","type":"array","items":{"type":"string"}},"capabilityDelegation":{"description":"Capability delegation methods","type":"array","items":{"type":"string"}},"keyAgreement":{"description":"Key agreement methods","type":"array","items":{"type":"string"}},"service":{"description":"Services associated with the DID document","type":"array","items":{"type":"string"}},"alsoKnownAs":{"description":"Alternative identifiers for the DID","type":"array","items":{"type":"string"}}},"required":["@context","id","controller","verificationMethod","authentication","assertionMethod","service"]}}},"paths":{"/did/generate":{"post":{"description":"Generates new DID documents for the specified methods.","operationId":"DidController_didGenerate","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateDidRequestDto"}}}},"responses":{"201":{"description":"The DID documents have been successfully created.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/DocumentObjectDto"}}}}},"400":{"description":"Validation error.","content":{"application/json":{}}},"401":{"description":"Missing or invalid authentication token.","content":{"application/json":{}}}},"summary":"Generate DID documents","tags":["DID Management"]}}}}
```

## Import DID documents from mnemonics

> Restores/creates DID documents for the specified methods using a mnemonic phrase.

```json
{"openapi":"3.0.0","info":{"title":"Wallet API","version":"1.0"},"security":[{"oneclick-jwt":[]}],"components":{"securitySchemes":{"oneclick-jwt":{"scheme":"bearer","bearerFormat":"JWT","type":"http","description":"One-Click access token (Authorization: Bearer <JWT>)"}},"schemas":{"ImportDidRequestDto":{"type":"object","properties":{"methods":{"description":"DID method which will be used for generate DID documents","type":"array","items":{"type":"string"}},"mnemonics":{"type":"string","description":"Mnemonic phrase used to restore the DID(s)."}},"required":["methods","mnemonics"]},"DocumentObjectDto":{"type":"object","properties":{"@context":{"type":"string","description":"JSON-LD context for the DID document"},"id":{"type":"string","description":"The DID"},"controller":{"type":"string","description":"The controller of the DID document"},"verificationMethod":{"type":"object","description":"The verification methods associated with the DID document"},"authentication":{"description":"Authentication methods for the DID document","type":"array","items":{"type":"string"}},"assertionMethod":{"description":"Assertion methods for the DID document","type":"array","items":{"type":"string"}},"capabilityInvocation":{"description":"Capability invocation methods","type":"array","items":{"type":"string"}},"capabilityDelegation":{"description":"Capability delegation methods","type":"array","items":{"type":"string"}},"keyAgreement":{"description":"Key agreement methods","type":"array","items":{"type":"string"}},"service":{"description":"Services associated with the DID document","type":"array","items":{"type":"string"}},"alsoKnownAs":{"description":"Alternative identifiers for the DID","type":"array","items":{"type":"string"}}},"required":["@context","id","controller","verificationMethod","authentication","assertionMethod","service"]}}},"paths":{"/did/import":{"post":{"description":"Restores/creates DID documents for the specified methods using a mnemonic phrase.","operationId":"DidController_didImport","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportDidRequestDto"}}}},"responses":{"201":{"description":"The DID documents have been successfully imported.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/DocumentObjectDto"}}}}},"400":{"description":"Validation error.","content":{"application/json":{}}},"401":{"description":"Missing or invalid authentication token.","content":{"application/json":{}}}},"summary":"Import DID documents from mnemonics","tags":["DID Management"]}}}}
```

## Retrieve a DID document by method

> Fetches the existing DID document for the specified method. For EMPE\_TESTNET, the underlying network must be TESTNET.

```json
{"openapi":"3.0.0","info":{"title":"Wallet API","version":"1.0"},"security":[{"oneclick-jwt":[]}],"components":{"securitySchemes":{"oneclick-jwt":{"scheme":"bearer","bearerFormat":"JWT","type":"http","description":"One-Click access token (Authorization: Bearer <JWT>)"}},"schemas":{"DocumentObjectDto":{"type":"object","properties":{"@context":{"type":"string","description":"JSON-LD context for the DID document"},"id":{"type":"string","description":"The DID"},"controller":{"type":"string","description":"The controller of the DID document"},"verificationMethod":{"type":"object","description":"The verification methods associated with the DID document"},"authentication":{"description":"Authentication methods for the DID document","type":"array","items":{"type":"string"}},"assertionMethod":{"description":"Assertion methods for the DID document","type":"array","items":{"type":"string"}},"capabilityInvocation":{"description":"Capability invocation methods","type":"array","items":{"type":"string"}},"capabilityDelegation":{"description":"Capability delegation methods","type":"array","items":{"type":"string"}},"keyAgreement":{"description":"Key agreement methods","type":"array","items":{"type":"string"}},"service":{"description":"Services associated with the DID document","type":"array","items":{"type":"string"}},"alsoKnownAs":{"description":"Alternative identifiers for the DID","type":"array","items":{"type":"string"}}},"required":["@context","id","controller","verificationMethod","authentication","assertionMethod","service"]}}},"paths":{"/dids/{method}":{"get":{"description":"Fetches the existing DID document for the specified method. For EMPE_TESTNET, the underlying network must be TESTNET.","operationId":"DidController_getDid","parameters":[{"name":"method","required":true,"in":"path","description":"The DID method to resolve.","schema":{"enum":["empe","empe-testnet","web"],"type":"string"}}],"responses":{"200":{"description":"The DID document has been successfully retrieved.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentObjectDto"}}}},"400":{"description":"The `method` query parameter is missing or invalid.","content":{"application/json":{}}},"404":{"description":"No DID document exists for the requested method.","content":{"application/json":{}}}},"summary":"Retrieve a DID document by method","tags":["DID Management"]}}}}
```

## Retrieve all DID documents for authenticated user

> Fetches all DID documents from all networks

```json
{"openapi":"3.0.0","info":{"title":"Wallet API","version":"1.0"},"security":[{"oneclick-jwt":[]}],"components":{"securitySchemes":{"oneclick-jwt":{"scheme":"bearer","bearerFormat":"JWT","type":"http","description":"One-Click access token (Authorization: Bearer <JWT>)"}},"schemas":{"DocumentObjectDto":{"type":"object","properties":{"@context":{"type":"string","description":"JSON-LD context for the DID document"},"id":{"type":"string","description":"The DID"},"controller":{"type":"string","description":"The controller of the DID document"},"verificationMethod":{"type":"object","description":"The verification methods associated with the DID document"},"authentication":{"description":"Authentication methods for the DID document","type":"array","items":{"type":"string"}},"assertionMethod":{"description":"Assertion methods for the DID document","type":"array","items":{"type":"string"}},"capabilityInvocation":{"description":"Capability invocation methods","type":"array","items":{"type":"string"}},"capabilityDelegation":{"description":"Capability delegation methods","type":"array","items":{"type":"string"}},"keyAgreement":{"description":"Key agreement methods","type":"array","items":{"type":"string"}},"service":{"description":"Services associated with the DID document","type":"array","items":{"type":"string"}},"alsoKnownAs":{"description":"Alternative identifiers for the DID","type":"array","items":{"type":"string"}}},"required":["@context","id","controller","verificationMethod","authentication","assertionMethod","service"]}}},"paths":{"/dids":{"get":{"description":"Fetches all DID documents from all networks","operationId":"DidController_getAllDocuments","parameters":[],"responses":{"200":{"description":"The DID documents have been successfully retrieved","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/DocumentObjectDto"}}}}}},"summary":"Retrieve all DID documents for authenticated user","tags":["DID Management"]}}}}
```

## GET /did/user/{id}/did.json

> Serve did:web did.json for a given user

```json
{"openapi":"3.0.0","info":{"title":"Wallet API","version":"1.0"},"security":[{"oneclick-jwt":[]}],"components":{"securitySchemes":{"oneclick-jwt":{"scheme":"bearer","bearerFormat":"JWT","type":"http","description":"One-Click access token (Authorization: Bearer <JWT>)"}}},"paths":{"/did/user/{id}/did.json":{"get":{"operationId":"DidController_getUserDidJson","parameters":[{"name":"id","required":true,"in":"path","description":"User id","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"summary":"Serve did:web did.json for a given user","tags":["DID Management"]}}}}
```


# Credential Management

## GET /credentials

> Retrieve all stored credentials for a DID

```json
{"openapi":"3.0.0","info":{"title":"Wallet API","version":"1.0"},"security":[{"oneclick-jwt":[]}],"components":{"securitySchemes":{"oneclick-jwt":{"scheme":"bearer","bearerFormat":"JWT","type":"http","description":"One-Click access token (Authorization: Bearer <JWT>)"}},"schemas":{"GetCredentialsResponseDto":{"type":"object","properties":{"credentials":{"description":"Array of verifiable credentials","type":"array","items":{"type":"string"}}},"required":["credentials"]}}},"paths":{"/credentials":{"get":{"operationId":"CredentialController_getCredentialCollection","parameters":[{"name":"network","required":false,"in":"query","description":"The network to use for DID creation","schema":{"type":"string"}},{"name":"type","required":false,"in":"query","description":"The type of credential to retrieve","schema":{"type":"string"}},{"name":"status","required":false,"in":"query","description":"The credential status to filter by","schema":{"type":"string","enum":["refresh","revocation","suspension","message"]}},{"name":"issuedAt","required":false,"in":"query","description":"The credential issuance date to filter by","schema":{"type":"string"}},{"name":"version","required":false,"in":"query","description":"The credential version to filter by","schema":{"type":"number","enum":[1,2]}}],"responses":{"200":{"description":"The credentials have been successfully retrieved","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetCredentialsResponseDto"}}}}},"summary":"Retrieve all stored credentials for a DID","tags":["Credential Management"]}}}}
```

## POST /credentials

> Import a credential from a JSON object

```json
{"openapi":"3.0.0","info":{"title":"Wallet API","version":"1.0"},"security":[{"oneclick-jwt":[]}],"components":{"securitySchemes":{"oneclick-jwt":{"scheme":"bearer","bearerFormat":"JWT","type":"http","description":"One-Click access token (Authorization: Bearer <JWT>)"}},"schemas":{"ImportCredentialDto":{"type":"object","properties":{"credential":{"description":"Credential to import","oneOf":[{"$ref":"#/components/schemas/VerifiableCredentialDto"}],"allOf":[{"$ref":"#/components/schemas/VerifiableCredentialDto"}]},"allowForeignSubject":{"type":"boolean","description":"If allowForeignSubject is true allow any credential. If false (default) check if credential has subject Id, if it has it must match one of active DIDs of the wallet","default":false}},"required":["credential","allowForeignSubject"]},"VerifiableCredentialDto":{"type":"object","properties":{"@context":{"description":"JSON-LD @context","oneOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"type":{"description":"Credential types","type":"array","items":{"type":"string"}},"id":{"type":"string","description":"Credential ID (URI)"},"issuer":{"description":"Issuer (DID or object with id)","oneOf":[{"$ref":"#/components/schemas/IssuerObjectDto"},{"$ref":"#/components/schemas/IssuerStringDto"}]},"issuanceDate":{"type":"string","description":"Issuance date"},"credentialSubject":{"description":"Credential subject(s)","oneOf":[{"$ref":"#/components/schemas/CredentialSubjectDto"},{"type":"array","items":{"$ref":"#/components/schemas/CredentialSubjectDto"}}]},"proof":{"description":"Proof","oneOf":[{"$ref":"#/components/schemas/ProofDto"}]},"expirationDate":{"type":"string","description":"Expiration date"},"credentialSchema":{"description":"Credential schema","oneOf":[{"$ref":"#/components/schemas/CredentialSchemaDto"}],"allOf":[{"$ref":"#/components/schemas/CredentialSchemaDto"}]},"credentialStatus":{"type":"array","description":"Credential status entries","items":{"$ref":"#/components/schemas/CredentialStatusEntryDto"}},"evidence":{"description":"Additional evidence","oneOf":[{"$ref":"#/components/schemas/FreeFormObjectDto"}]},"_ext":{"type":"object","additionalProperties":true}},"required":["@context","type","id","issuer","issuanceDate","credentialSubject"]},"IssuerObjectDto":{"type":"object","properties":{"id":{"type":"string","description":"Issuer DID"}},"required":["id"]},"IssuerStringDto":{"type":"object","properties":{"value":{"type":"string","description":"Issuer DID"}},"required":["value"]},"CredentialSubjectDto":{"type":"object","properties":{"id":{"type":"string","description":"Subject DID"},"_":{"type":"object","description":"Additional subject claims","additionalProperties":true}},"required":["_"]},"ProofDto":{"type":"object","properties":{"_":{"type":"object","additionalProperties":true}},"required":["_"]},"CredentialSchemaDto":{"type":"object","properties":{"id":{"type":"string"},"type":{"type":"string"}},"required":["id","type"]},"CredentialStatusEntryDto":{"type":"object","properties":{"_":{"type":"object","additionalProperties":true}},"required":["_"]},"FreeFormObjectDto":{"type":"object","properties":{"_":{"type":"object","additionalProperties":true}},"required":["_"]},"ImportCredentialResponseDto":{"type":"object","properties":{"id":{"type":"string","description":"Credential identifier"}},"required":["id"]}}},"paths":{"/credentials":{"post":{"operationId":"CredentialController_importCredential","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportCredentialDto"}}}},"responses":{"201":{"description":"Credential imported successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportCredentialResponseDto"}}}},"400":{"description":"Invalid credential (missing recipient/schema or subject mismatch)"},"409":{"description":"Credential with this id already exists"},"422":{"description":"Proof verification failed"}},"summary":"Import a credential from a JSON object","tags":["Credential Management"]}}}}
```

## DELETE /credentials

> Delete all credentials

```json
{"openapi":"3.0.0","info":{"title":"Wallet API","version":"1.0"},"security":[{"oneclick-jwt":[]}],"components":{"securitySchemes":{"oneclick-jwt":{"scheme":"bearer","bearerFormat":"JWT","type":"http","description":"One-Click access token (Authorization: Bearer <JWT>)"}}},"paths":{"/credentials":{"delete":{"operationId":"CredentialController_deleteAllCredentials","parameters":[],"responses":{"204":{"description":"All credentials have been successfully deleted."},"401":{"description":"Missing or invalid authentication token.","content":{"application/json":{}}}},"summary":"Delete all credentials","tags":["Credential Management"]}}}}
```

## GET /credentials/{id}

> Retrieve a credential by its ID

```json
{"openapi":"3.0.0","info":{"title":"Wallet API","version":"1.0"},"security":[{"oneclick-jwt":[]}],"components":{"securitySchemes":{"oneclick-jwt":{"scheme":"bearer","bearerFormat":"JWT","type":"http","description":"One-Click access token (Authorization: Bearer <JWT>)"}},"schemas":{"GetCredentialResponseDto":{"type":"object","properties":{"credential":{"type":"object","description":"Verifiable credential"}},"required":["credential"]}}},"paths":{"/credentials/{id}":{"get":{"operationId":"CredentialController_getCredential","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"The credential has been successfully retrieved","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetCredentialResponseDto"}}}}},"summary":"Retrieve a credential by its ID","tags":["Credential Management"]}}}}
```

## DELETE /credentials/{id}

> Delete a specific credential

```json
{"openapi":"3.0.0","info":{"title":"Wallet API","version":"1.0"},"security":[{"oneclick-jwt":[]}],"components":{"securitySchemes":{"oneclick-jwt":{"scheme":"bearer","bearerFormat":"JWT","type":"http","description":"One-Click access token (Authorization: Bearer <JWT>)"}}},"paths":{"/credentials/{id}":{"delete":{"operationId":"CredentialController_deleteCredential","parameters":[{"name":"id","required":true,"in":"path","description":"Credential identifier to delete","schema":{"type":"string"}}],"responses":{"204":{"description":"The credential has been successfully deleted."},"401":{"description":"Missing or invalid authentication token.","content":{"application/json":{}}},"404":{"description":"Credential not found.","content":{"application/json":{}}}},"summary":"Delete a specific credential","tags":["Credential Management"]}}}}
```


# Flow

## Process QR code content and initiate a credential flow

> Analyzes QR code data and starts either credential issuance or verification process

```json
{"openapi":"3.0.0","info":{"title":"Wallet API","version":"1.0"},"security":[{"oneclick-jwt":[]}],"components":{"securitySchemes":{"oneclick-jwt":{"scheme":"bearer","bearerFormat":"JWT","type":"http","description":"One-Click access token (Authorization: Bearer <JWT>)"}},"schemas":{"ProcessQrContentDto":{"type":"object","properties":{"qrContent":{"type":"string","description":"QR code content (URL) to process"},"did":{"type":"string","description":"Did of the holder"}},"required":["qrContent","did"]},"ProcessQrContentResponseDto":{"type":"object","properties":{"flowType":{"type":"string","description":"Type of the flow (claim or presentation)","enum":["claim","presentation"]},"data":{"description":"Result of the process","oneOf":[{"$ref":"#/components/schemas/ClaimResponseData"},{"$ref":"#/components/schemas/PresentationResponseData"},{"type":"boolean"}]}},"required":["flowType","data"]},"ClaimResponseData":{"type":"object","properties":{"processId":{"type":"object","description":"Unique identifier for the credential claim process","nullable":true},"status":{"type":"string","description":"Status of the claim process","enum":["initialized","confirmed","failed","expired"]},"offering":{"description":"Credential offering data","nullable":true,"allOf":[{"$ref":"#/components/schemas/OfferingExample"}]},"errorMessage":{"type":"string","description":"Error message - only available if status is failed"}},"required":["processId","status"]},"OfferingExample":{"type":"object","properties":{"credential_type":{"type":"string"},"credential_subject":{"$ref":"#/components/schemas/CredentialSubjectExample"}},"required":["credential_type","credential_subject"]},"CredentialSubjectExample":{"type":"object","properties":{"ticket":{"type":"string"},"seat":{"type":"string"},"description":{"type":"string"}},"required":["ticket","seat","description"]},"PresentationResponseData":{"type":"object","properties":{"processId":{"type":"object","description":"Unique identifier for the credential presentation process","nullable":true},"matchingCredentialIds":{"description":"Matching credential IDs - only available after the credential has been issued","type":"array","items":{"type":"string"}},"matchingCredentials":{"description":"Matching credentials - only available after the credential has been issued","nullable":true,"type":"array","items":{"$ref":"#/components/schemas/VerifiableCredentialExample"}},"status":{"type":"string","description":"Status of the presentation process","enum":["initialized","confirmed","failed","expired"]},"errorMessage":{"type":"string","description":"Error message - only available if status is failed"}},"required":["processId","matchingCredentialIds","status"]},"VerifiableCredentialExample":{"type":"object","properties":{"@context":{"description":"Credential context","type":"array","items":{"type":"string"}},"id":{"type":"string","description":"Credential ID"},"type":{"description":"Credential types","type":"array","items":{"type":"string"}},"issuer":{"description":"Issuer information","allOf":[{"$ref":"#/components/schemas/IssuerExample"}]},"issuanceDate":{"type":"string","description":"Issuance date of the credential"},"credentialSubject":{"description":"Subject data of the credential","allOf":[{"$ref":"#/components/schemas/CredentialSubjectExample"}]}},"required":["@context","id","type","issuer","issuanceDate","credentialSubject"]},"IssuerExample":{"type":"object","properties":{"id":{"type":"string"}},"required":["id"]}}},"paths":{"/flows":{"post":{"description":"Analyzes QR code data and starts either credential issuance or verification process","operationId":"FlowController_processQrContent","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProcessQrContentDto"}}}},"responses":{"201":{"description":"The credential flow has been successfully initiated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProcessQrContentResponseDto"}}}}},"summary":"Process QR code content and initiate a credential flow","tags":["Flow"]}}}}
```

## Confirm a credential claim or presentation

> Completes an initiated credential flow by confirming user selection

```json
{"openapi":"3.0.0","info":{"title":"Wallet API","version":"1.0"},"security":[{"oneclick-jwt":[]}],"components":{"securitySchemes":{"oneclick-jwt":{"scheme":"bearer","bearerFormat":"JWT","type":"http","description":"One-Click access token (Authorization: Bearer <JWT>)"}},"schemas":{"ConfirmProcessDto":{"type":"object","properties":{"type":{"type":"string","description":"Type of flow (claim or presentation)","enum":["claim","presentation"]},"selectedCredential":{"type":"object","description":"Complete credential object for testing and validation"}},"required":["type","selectedCredential"]},"ConfirmProcessResponseDto":{"type":"object","properties":{"status":{"type":"string","description":"Status of the confirmation","enum":["initialized","confirmed","failed","expired"]},"message":{"type":"string","description":"Message about the confirmation result"},"data":{"description":"Optional data field that contains additional information, such as detailed error data","allOf":[{"$ref":"#/components/schemas/VerifierErrorResponse"}]}},"required":["status","message"]},"VerifierErrorResponse":{"type":"object","properties":{"name":{"type":"string","description":"Error type name"},"value":{"type":"string","description":"Error value or code"},"field":{"type":"string","description":"Field that caused the error, if applicable"},"message":{"type":"string","description":"Detailed error message"}},"required":["name","value","field","message"]}}},"paths":{"/flows/{processId}/confirm":{"post":{"description":"Completes an initiated credential flow by confirming user selection","operationId":"FlowController_confirmProcess","parameters":[{"name":"processId","required":true,"in":"path","description":"The process ID to confirm","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfirmProcessDto"}}}},"responses":{"200":{"description":"The credential has been successfully confirmed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfirmProcessResponseDto"}}}}},"summary":"Confirm a credential claim or presentation","tags":["Flow"]}}}}
```


# Health

## GET /health/liveness

> Liveness probe

```json
{"openapi":"3.0.0","info":{"title":"Wallet API","version":"1.0"},"paths":{"/health/liveness":{"get":{"operationId":"HealthController_liveness","parameters":[],"responses":{"200":{"description":"The Health Check is successful","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"info":{"type":"object","additionalProperties":{"type":"object","required":["status"],"properties":{"status":{"type":"string"}},"additionalProperties":true},"nullable":true},"error":{"type":"object","additionalProperties":{"type":"object","required":["status"],"properties":{"status":{"type":"string"}},"additionalProperties":true},"nullable":true},"details":{"type":"object","additionalProperties":{"type":"object","required":["status"],"properties":{"status":{"type":"string"}},"additionalProperties":true}}}}}}},"503":{"description":"The Health Check is not successful","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"info":{"type":"object","additionalProperties":{"type":"object","required":["status"],"properties":{"status":{"type":"string"}},"additionalProperties":true},"nullable":true},"error":{"type":"object","additionalProperties":{"type":"object","required":["status"],"properties":{"status":{"type":"string"}},"additionalProperties":true},"nullable":true},"details":{"type":"object","additionalProperties":{"type":"object","required":["status"],"properties":{"status":{"type":"string"}},"additionalProperties":true}}}}}}}},"summary":"Liveness probe","tags":["Health"]}}}}
```

## GET /health/readiness

> Readiness probe (checks DB)

```json
{"openapi":"3.0.0","info":{"title":"Wallet API","version":"1.0"},"paths":{"/health/readiness":{"get":{"operationId":"HealthController_readiness","parameters":[],"responses":{"200":{"description":"The Health Check is successful","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"info":{"type":"object","additionalProperties":{"type":"object","required":["status"],"properties":{"status":{"type":"string"}},"additionalProperties":true},"nullable":true},"error":{"type":"object","additionalProperties":{"type":"object","required":["status"],"properties":{"status":{"type":"string"}},"additionalProperties":true},"nullable":true},"details":{"type":"object","additionalProperties":{"type":"object","required":["status"],"properties":{"status":{"type":"string"}},"additionalProperties":true}}}}}}},"503":{"description":"The Health Check is not successful","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"info":{"type":"object","additionalProperties":{"type":"object","required":["status"],"properties":{"status":{"type":"string"}},"additionalProperties":true},"nullable":true},"error":{"type":"object","additionalProperties":{"type":"object","required":["status"],"properties":{"status":{"type":"string"}},"additionalProperties":true},"nullable":true},"details":{"type":"object","additionalProperties":{"type":"object","required":["status"],"properties":{"status":{"type":"string"}},"additionalProperties":true}}}}}}}},"summary":"Readiness probe (checks DB)","tags":["Health"]}}}}
```




---

[Next Page](/llms-full.txt/1)

