> 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/verify-endpoint.md).

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

```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.


---

# 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/verify-endpoint.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.
