OID4VP Session API
The OID4VP Session API is the primary integration surface for wallet-based authentication. It lives on the Auth Bridge service at base path /auth/oid4vp/sessions (port 8090) and manages the full lifecycle of a verifiable presentation session: creation, status polling, and completion. Every wallet login begins with the frontend (via the BFF) creating a session, rendering the returned QR code, polling until the wallet holder has presented their credentials, and finally completing the session to obtain resolved identity claims.
These endpoints are designed for internal service-to-service communication. The portal's BFF layer calls them on behalf of the browser; external clients should not call them directly.
POST /auth/oid4vp/sessions
Creates a new OID4VP session. The Auth Bridge generates a presentation request according to the referenced DCQL query configuration, produces a QR code encoding the request_uri, and returns all the data the frontend needs to render the QR and begin polling.
Request Body
{
"queryId": "portal-eduid-vc",
"oauthSessionId": "optional-correlation-id",
"forceReconciliation": false
}
| Field | Type | Required | Description |
|---|---|---|---|
queryId | string | Yes | References a named DCQL (Digital Credentials Query Language) query in the Auth Bridge configuration. This determines which credential types and claims are requested from the wallet. |
oauthSessionId | string | No | An opaque correlation identifier, typically the STS session ID, used to tie the OID4VP session back to the OAuth2 authorization flow that triggered it. |
forceReconciliation | boolean | No | When true, forces the reconciliation flow even if the wallet holder already has a known binding. Useful for re-verification or account-linking scenarios. Defaults to false. |
The queryId is the key link between the session API and the credential request logic. It tells the Auth Bridge which presentation definition to embed in the authorization request object. See the DCQL Query Configuration section below for details.
Response (200 OK)
{
"sessionId": "550e8400-e29b-41d4-a716-446655440000",
"qrCodeDataUri": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...",
"requestUri": "openid4vp://authorize?request_uri=https://auth-bridge.example.com/auth/oid4vp/requests/550e8400...",
"statusUri": "/auth/oid4vp/sessions/550e8400-e29b-41d4-a716-446655440000/status",
"qrPageUri": "/auth/oid4vp/qr/550e8400-e29b-41d4-a716-446655440000"
}
| Field | Type | Description |
|---|---|---|
sessionId | string (UUID) | Unique identifier for this session. Used in all subsequent status and completion calls. |
qrCodeDataUri | string | A Base64-encoded PNG image of the QR code, ready to be rendered in an <img> tag's src attribute. The QR encodes the requestUri. |
requestUri | string | The full openid4vp:// deep link that the wallet app will open. On mobile devices, this can be used directly as a clickable link instead of the QR code. |
statusUri | string | The relative path to poll for session status updates. The frontend should append this to the Auth Bridge base URL. |
qrPageUri | string | A relative path to a standalone HTML page that renders the QR code. Useful for iframe embedding or as a fallback. |
How It Works
When this endpoint is called, the Auth Bridge performs the following steps internally:
- Looks up the DCQL query configuration identified by
queryId. - Generates a cryptographic nonce and creates an OID4VP authorization request object.
- Stores the request object at a unique
request_urithat the wallet will fetch. - Generates a QR code image encoding the
openid4vp://URI. - Creates a session record in the in-memory session store with status
CREATED. - Returns the session metadata to the caller.
GET /auth/oid4vp/sessions/{sessionId}/status
Polls the current status of an OID4VP session. The frontend calls this endpoint at regular intervals after rendering the QR code, waiting for the wallet holder to scan and present their credentials.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
sessionId | string (UUID) | The session identifier returned by the creation endpoint. |
Response (200 OK)
{
"sessionId": "550e8400-e29b-41d4-a716-446655440000",
"status": "VERIFIED",
"idvRequired": false,
"idvRequirementReason": null,
"reconciliationPlanType": "USE_EXISTING_BINDING"
}
| Field | Type | Description |
|---|---|---|
sessionId | string (UUID) | Echo of the requested session ID. |
status | string (enum) | Current session status. See the Status Values table below. |
idvRequired | boolean | Whether identity verification is needed before the session can be completed. Only meaningful when status is VERIFIED or IDV_REQUIRED. |
idvRequirementReason | string or null | Human-readable explanation of why IDV is required, e.g. "No existing binding found for this wallet holder". Null when IDV is not required. |
reconciliationPlanType | string or null | The reconciliation strategy the Auth Bridge has selected. Values include USE_EXISTING_BINDING, RECONCILE_VIA_IDV, AUTO_MATCH_BY_ATTRIBUTE. Null until the status reaches VERIFIED. |
Polling Strategy
The recommended polling interval is 2 seconds. This provides responsive feedback to the user without placing excessive load on the Auth Bridge. The frontend should:
- Start polling immediately after rendering the QR code.
- Continue polling while the status is
CREATED,PENDING,INTERACTION_STARTED, orVERIFYING. - Stop polling when the status reaches a terminal state:
VERIFIED,IDV_REQUIRED,COMPLETED,EXPIRED, orERROR. - Implement a maximum polling duration (recommended: 5 minutes) as a client-side safeguard against stuck sessions.
const pollStatus = async (sessionId: string): Promise<SessionStatus> => {
const MAX_POLL_DURATION_MS = 5 * 60 * 1000;
const POLL_INTERVAL_MS = 2000;
const startTime = Date.now();
while (Date.now() - startTime < MAX_POLL_DURATION_MS) {
const response = await fetch(`/api/wallet/sessions/${sessionId}/status`);
const data = await response.json();
if (['VERIFIED', 'IDV_REQUIRED', 'COMPLETED', 'EXPIRED', 'ERROR'].includes(data.status)) {
return data;
}
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
}
throw new Error('Session polling timed out');
};
POST /auth/oid4vp/sessions/{sessionId}/complete
Completes the wallet authentication session and returns the resolved identity claims. This endpoint must be called after the status has reached VERIFIED (or COMPLETED if auto-reconciliation succeeded). The response varies depending on whether the wallet holder is already known to the system.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
sessionId | string (UUID) | The session identifier. |
Response for Known Holder (200 OK)
When the wallet holder has an existing identity binding, the Auth Bridge resolves their canonical identity and returns the full claim set:
{
"userId": "internal-identity-id",
"claims": {
"eduid": "urn:mace:surf.nl:eduid:12345",
"eduperson_principal_name": "student@institution.nl",
"email": "student@institution.nl"
},
"isNewUser": false,
"authenticatedAt": "2026-03-27T10:30:00Z",
"acr": "urn:sphereon:oid4vp:vp",
"amr": ["vp"],
"claimSource": "CANONICAL_BINDING"
}
| Field | Type | Description |
|---|---|---|
userId | string | The internal identity identifier that the STS will embed as the sub claim in the issued tokens. |
claims | object | The resolved identity claims. The exact set depends on the DCQL query and the reconciliation outcome. |
isNewUser | boolean | false for known holders with an existing binding, true for newly reconciled identities. |
authenticatedAt | string (ISO 8601) | Timestamp of the wallet authentication event. |
acr | string | Authentication Context Class Reference, always urn:sphereon:oid4vp:vp for wallet-based login. |
amr | array of strings | Authentication Methods References. Includes "vp" for verifiable presentation. |
claimSource | string | Indicates where the claims were resolved from. CANONICAL_BINDING means the identity was found via an existing wallet-to-identity link. |
Response for Unknown Holder (202 Accepted)
When the wallet holder is not yet known to the system, the Auth Bridge cannot resolve an identity and instead signals that identity verification is required:
{
"idvRequired": true,
"idvMethod": "oidc",
"idvSteps": ["Authenticate with your institution account to link your wallet"]
}
| Field | Type | Description |
|---|---|---|
idvRequired | boolean | Always true in this response. |
idvMethod | string | The verification method to use. Currently always "oidc", meaning the user must authenticate with an external OIDC provider (e.g., SURFconext). |
idvSteps | array of strings | Human-readable instructions that the frontend can display to guide the user through the verification process. |
The 200 vs 202 distinction is deliberate and meaningful. A 200 OK response means the authentication is fully resolved -- the STS can issue tokens immediately. A 202 Accepted response means the request was valid but the identity resolution is incomplete -- the frontend must now redirect the user into the IDV Reconciliation flow before the session can be completed. After IDV completes successfully, the frontend should call this endpoint again to obtain the 200 response with full claims.
Session Status Values
The following table describes every status value a session can have, along with the expected frontend behavior for each.
| Status | Description | Frontend Action |
|---|---|---|
CREATED | Session has been created; QR code is ready for scanning. | Display QR code, begin polling. |
PENDING | Alias for CREATED. Waiting for the wallet to scan. | Continue polling. |
INTERACTION_STARTED | The wallet has fetched the request object from the request_uri. | Optionally update the UI to indicate the wallet is processing. Continue polling. |
VERIFYING | The verifiable presentation has been received and is being validated (signature checks, credential status, revocation). | Show a "Verifying..." indicator. Continue polling. |
VERIFIED | The VP is valid and the identity resolution process has begun. Check idvRequired to determine next steps. | Stop polling. If idvRequired is false, call the complete endpoint. If true, begin the IDV flow. |
IDV_REQUIRED | The wallet holder is unknown and identity verification must be performed before completion. | Stop polling. Redirect user to the IDV flow. |
COMPLETED | The session is fully resolved. Claims are available via the complete endpoint. | Stop polling. Call the complete endpoint. |
EXPIRED | The session has timed out. The default timeout is 5 minutes from creation. | Stop polling. Show an expiration message. Offer to create a new session. |
ERROR | Verification or processing failed. | Stop polling. Show an error message. Offer to retry. |
Error Responses
All error responses follow a consistent JSON structure:
{
"error": "session_not_found",
"error_description": "No OID4VP session exists with the given identifier"
}
| HTTP Status | Error Code | When It Occurs |
|---|---|---|
404 Not Found | session_not_found | The sessionId does not match any known session. The session may have been cleaned up after expiration. |
409 Conflict | invalid_session_state | The operation is not valid for the session's current status. For example, calling complete on a session that is still in CREATED status. |
410 Gone | session_expired | The session existed but has expired. Unlike 404, this confirms the session did exist. The client should create a new session. |
400 Bad Request | invalid_request | The request body is malformed or missing required fields. |
500 Internal Server Error | server_error | An unexpected error occurred during processing. The error is logged server-side for investigation. |
DCQL Query Configuration
The queryId parameter in the session creation request references a named DCQL (Digital Credentials Query Language) query defined in the Auth Bridge configuration. DCQL is the mechanism used in OpenID4VP to specify which credentials and claims the verifier (Auth Bridge) wants the wallet to present.
A typical configuration entry looks like this:
oid4vp:
queries:
portal-eduid-vc:
credentials:
- id: "eduid-credential"
format: "jwt_vc"
type: "EduIDCredential"
claims:
- path: "$.credentialSubject.eduid"
required: true
- path: "$.credentialSubject.eduperson_principal_name"
required: true
- path: "$.credentialSubject.email"
required: false
- path: "$.credentialSubject.given_name"
required: false
- path: "$.credentialSubject.family_name"
required: false
trusted_issuers:
- "did:web:eduid.nl"
- "did:web:test.eduid.nl"
This configuration tells the Auth Bridge to request a jwt_vc-format credential of type EduIDCredential, requiring at minimum the eduid and eduperson_principal_name claims, while optionally requesting email, given_name, and family_name. Only credentials issued by the listed trusted issuers will be accepted.
The query configuration is entirely server-side. The frontend does not need to know which credentials are being requested; it simply passes the queryId and lets the Auth Bridge handle the rest. This separation of concerns keeps the presentation logic out of the browser and allows the credential requirements to be changed without redeploying the frontend.