> 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/issuing-credentials.md).

# 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.md) and [uploading the schema](/getting-started/tutorial/uploading-schema.md):

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


---

# 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/issuing-credentials.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.
