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[email protected]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.jsthat:Loads environment variables (via
dotenv).Serves static files from a
public/directory.Mounts our Issuer router (where the
/kyc-credentialroute 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:
Make sure your .env file holds the values we saved when deploying the Issuer and uploading the schema:
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
[email protected]schema, with those values as the credential's claims.Returns the offer data, including a
credentialOfferUriand 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 ([email protected]: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, inname@version:formatform, e.g.["[email protected]:sd-jwt"].requirePin(boolean, optional, defaultfalse) — whentrue, 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. UseclaimsByConfigurationIdto supply the actual claim values per configuration id.
Response Body:
credentialOfferUri— anopenid-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 whenrequirePinwastrue.
Example request body for our KYC credential:
Example response:
Implementing the route
Steps: Update src/issuer-routes.js:
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 a400 × 400PNG image instead of JSON. The offer URI is returned in theX-Credential-Offer-Uriresponse header, the session id inX-Issuance-Session-Id, and (when a PIN is required) the PIN inX-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 theX-Credential-Offer-Uriheader.
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:
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:
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 itsstate(for exampleOfferCreatedwhile waiting, thenCompletedonce the wallet claims it),credentialOfferUri,createdAt,expiresAt, andissuedCredentialswhen finished.GET {ISSUER_BASE_URL}/issuance-sessions?issuerId={issuerDid}— lists all sessions for your Issuer.
A quick check from the terminal:
Run the server
Next, we will build a simple frontend form to submit the user data and display the QR code.
Last updated