> For the complete documentation index, see [llms.txt](https://docs.empe.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.empe.io/getting-started/tutorial/verification-flow.md).

# 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.md). 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.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.empe.io/getting-started/tutorial/verification-flow.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
