> 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/develop/verifier/frontend-integration.md).

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


---

# 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/develop/verifier/frontend-integration.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.
