Create a Request
This step prepares a presentation request in your backend. Keep the request session, signed request envelope, and Presentation Definition on the server.
The backend flow is:
- Prepare the presentation request and submission URL for Credential Wallet.
- Create a Presentation Request Envelope with
createRequestFromConfig. - Store it in your backend.
Backend routes
Your application owns both the verification request and the browser session. Journal exposes these app-owned routes (with trailing slashes):
| Endpoint | Purpose |
|---|---|
POST /api/personhood/sessions/ | Create a verification request and return its PD fetch URL to the frontend. |
GET /api/personhood/sessions/{sessionId}/pd/ | Return the wallet-compatible presentation request envelope. |
POST /api/personhood/sessions/{sessionId}/submit/ | Receive and verify the wallet presentation submission. |
GET /api/personhood/sessions/{sessionId}/status/ | Return request status to the owning browser. |
POST /api/personhood/sessions/{sessionId}/complete/ | Establish a browser session after verification. |
GET /api/personhood/session/ | Return the current browser user’s name and session expiry, or user: null. |
POST /api/personhood/profile/ | Save the public name when missing. |
POST /api/personhood/logout/ | Delete the current session and clear cookies. |
The examples below use a BasicAccessRequestVerifiableCredential request. Replace the route names and request type with the values registered for your app.
Prepare the Presentation Request and Submission URL for Credential Wallet
When the user starts verification, create an app-owned request. Generate a unique request ID and nonce, and choose an expiration time within the system limits.
The presentation request expiration must be in the future and no more than one hour from creation. The requested credential's minimum lifetime must also be in the future and no more than 30 days from creation. Use the SDK's expiresIn helper rather than calculating milliseconds directly.
Both URLs must be HTTPS URLs on domains registered in the app config:
pdFetchUrlis the URL that Credential Wallet calls to fetch the presentation request.submissionUrlis the URL that Credential Wallet calls to submit the signed presentation.
import { expiresIn } from '@iamhuman.link/presentation-exchange-sdk';
import { presentation } from './presentation-service.js';
const sessionId = crypto.randomUUID();
const subject = `${presentation.appConfig.appId}:journal-comments`;
const nonce = crypto.randomUUID();
const expiresAt = expiresIn({ minutes: 10 });
const credentialMinimumLifetime = expiresIn({ days: 7, minutes: 10 });
const apiBaseUrl = 'https://blog.iamhuman.link'; // Use your registered HTTPS origin.
const pdFetchUrl = `${apiBaseUrl}/api/personhood/sessions/${sessionId}/pd/`;
const submissionUrl = `${apiBaseUrl}/api/personhood/sessions/${sessionId}/submit/`;
subject is a stable app-owned authorization scope. Journal uses <appId>:journal-comments because the user’s identity is not known yet. Do not substitute a random request ID or assume a pre-existing account ID. The ten-minute request and the minimum credential lifetime of seven days plus ten minutes allow for a seven-day browser session after verification.
Create a presentation request
Use the SDK service initialized in Add the SDK and load app config.
For a config-driven request, the registered app config supplies the requested attributes and personal-data policy. Do not pass attributes or construct a second copy of those policy values in application code.
const requestType = 'BasicAccessRequestVerifiableCredential';
const { envelope, presentationDefinition, verificationContext } = await presentation.createRequestFromConfig({
requestType,
subject,
pdRequestId: sessionId,
nonce,
expiresAt,
pdFetchUrl,
submissionUrl,
definition: {
id: sessionId,
expirationMinimum: credentialMinimumLifetime,
},
});
Store it in your backend
Store the returned envelope in your backend. It has this shape:
{
jwtVc: string;
expiresAt: string;
pdRequestId: string;
pdRequestType: string;
pdHash: string;
appId: string;
nonce: string;
}
Persist the envelope, presentationDefinition, and verificationContext in your backend request storage, together with subject, nonce, submissionUrl, expiresAt, and initial status: "pending". Journal stores requests in Firestore.
Bind each request to its initiating browser: generate a random token, set it in the HttpOnly journal_verification cookie for ten minutes, and store its hash as the request owner. Check ownership on status and completion routes. Require a trusted Origin on browser mutation routes. The wallet fetch and submission routes must work without browser cookies or an Origin header; authenticate submissions through the stored presentation bindings. Use Cache-Control: no-store on personhood API responses.
The stored presentationDefinition and envelope are the values that bind the later wallet submission to this session. The verificationContext is the SDK's config-derived verification input; pass its policy and targetCredentialType to verifySubmission in document 08 instead of duplicating policy values in application code.
Return only what the frontend needs to display the request:
return Response.json({
session: { sessionId, pdFetchUrl, expiresAt: envelope.expiresAt },
}, { status: 201, headers: { 'Cache-Control': 'no-store' } });
The response must also set the browser-ownership cookie described above. This response excerpt omits cookie construction; it does not replace that ownership check.
Serve the request envelope
Credential Wallet fetches the request through pdFetchUrl. Look up the request, then return the stored envelope under pdVc.
// After loading the request and rejecting unknown, expired, or non-pending records:
return Response.json({ ok: true, pdVc: request.envelope }, {
headers: { 'Cache-Control': 'no-store' },
});
The frontend and wallet use pdFetchUrl; they do not need the signed jwtVc directly. Continue with Display the request and scan it from Credential Wallet.