Skip to main content
Version: v0.25.0 (Latest)

Universal OID4VP

The Universal OID4VP module provides a backend-focused API for web applications and services that need to verify credentials without managing the low-level protocol details.

Overview

While the standard Verifier API gives you full control over the OID4VP protocol, the Universal API simplifies common use cases:

  • Session Management - Handles session creation and lifecycle automatically
  • QR Code Generation - Creates QR codes with customizable styling
  • Status Polling - Exposes a single endpoint that returns the current verification state
  • Webhook Callbacks - Posts to a URL you configure when verification completes

Use Cases

The Universal API is ideal for:

ScenarioDescription
Web ApplicationsDisplay QR code, poll for result, redirect on success
Kiosk SystemsStateless verification with QR display
Backend ServicesWebhook-based verification for async workflows
Mobile WebSame-device flows with deep links

Architecture

Universal OID4VP Architecture

REST Endpoints

The Universal API exposes three endpoints that map to the lifecycle of a verification session: create one, check its status, and clean it up when you're done.

Create Authorization Request

This is the starting point for any verification flow. You post a DCQL query (either by referencing a pre-configured query_id or by inlining the query directly) and get back everything you need to present a QR code to the user and track the session.

POST /oid4vp/backend/auth/requests
Content-Type: application/json

{
"query_id": "age-verification",
"client_id": "https://verifier.example.com",
"callback": {
"url": "https://my-app.example.com/webhook",
"statuses": ["AUTHORIZATION_RESPONSE_VERIFIED"],
"include_verified_data": true
},
"ttl_seconds": 600,
"qr_code": {
"size": 400,
"color_dark": "#000000",
"color_light": "#ffffff"
}
}

Request Fields:

FieldTypeRequiredDescription
query_idstringEither query_id or dcql_queryReference to pre-configured DCQL query
dcql_queryobjectEither query_id or dcql_queryInline DCQL query
client_idstringNoOverride default client ID
callbackobjectNoWebhook configuration
ttl_secondsnumberNoSession TTL (default: 600)
qr_codeobjectNoQR code styling options

Response:

{
"correlation_id": "sess-abc123",
"query_id": "age-verification",
"request_uri": "openid4vp://authorize?request_uri=https%3A%2F%2Fverifier.example.com%2Foid4vp%2Frequests%2Fsess-abc123",
"qr_code_content": "openid4vp://authorize?request_uri=...",
"qr_code_data_uri": "data:image/png;base64,iVBORw0KGgo...",
"status_uri": "https://verifier.example.com/oid4vp/backend/auth/requests/sess-abc123",
"expires_at": 1704200400000,
"status": "CREATED"
}

Get Authorization Request Status

After creating a session, your frontend (or backend) needs to know when the user has scanned the QR code and whether verification succeeded. Poll this endpoint on an interval, or use it as a one-off check if you're primarily relying on webhooks.

GET /oid4vp/backend/auth/requests/{correlationId}

Response:

{
"correlation_id": "sess-abc123",
"query_id": "age-verification",
"status": "AUTHORIZATION_RESPONSE_VERIFIED",
"created_at": 1704199800000,
"last_updated": 1704199850000,
"expires_at": 1704200400000,
"verified_data": {
"credentials": [
{
"id": "age_over_18",
"format": "dc+sd-jwt",
"type": "VerifiedPerson",
"claims": {
"age_over_18": true,
"given_name": "John"
}
}
]
}
}

Delete Authorization Request

Sessions expire automatically based on ttl_seconds, but you can also remove them explicitly. This is useful if a user cancels a flow or navigates away, and you want to free up the session immediately rather than waiting for the TTL.

DELETE /oid4vp/backend/auth/requests/{correlationId}

Session Status Values

A session moves through these states in order. Your polling logic or webhook handler should look for the terminal states (AUTHORIZATION_RESPONSE_VERIFIED, ERROR, EXPIRED) to decide what to do next.

StatusDescription
CREATEDSession created, waiting for wallet scan
REQUEST_RETRIEVEDWallet has retrieved the authorization request
AUTHORIZATION_RESPONSE_RECEIVEDWallet submitted response, verification in progress
AUTHORIZATION_RESPONSE_VERIFIEDCredentials verified successfully
ERRORVerification failed
EXPIREDSession timed out

Usage Examples

Web Application Flow

The most common pattern is a two-step flow: create a session to get a QR code, then poll for the result. The examples below show both the Kotlin SDK and a plain JavaScript approach using fetch against the REST API.

// 1. Create session and get QR code
val createCommand: CreateAuthRequestEndpointCommand = session.graph.createAuthRequestCommand

val result = createCommand.execute(
CreateAuthorizationRequestInput(
queryId = "age-verification",
qrCodeOptions = QrCodeOptions(size = 300)
),
sessionContext
)

val output = result.getOrThrow()
println("Display QR: ${output.qrCodeDataUri}")
println("Poll status at: ${output.statusUri}")

// 2. Poll for completion
val statusCommand: GetAuthRequestStatusEndpointCommand = session.graph.getAuthRequestStatusCommand

while (true) {
delay(2000)
val status = statusCommand.execute(
correlationId = output.correlationId,
sessionContext
).getOrThrow()

when (status.status) {
AuthorizationSessionStatus.AUTHORIZATION_RESPONSE_VERIFIED -> {
println("Verified: ${status.verifiedData}")
break
}
AuthorizationSessionStatus.ERROR -> {
println("Error: ${status.error}")
break
}
AuthorizationSessionStatus.EXPIRED -> {
println("Session expired")
break
}
else -> continue
}
}

Webhook Integration

If you don't want to poll, you can register a callback URL when creating the session. The server will POST to that URL whenever the session reaches one of the statuses you specify. This is a better fit for backend services where you don't have a browser polling loop.

val result = createCommand.execute(
CreateAuthorizationRequestInput(
queryId = "kyc-verification",
callback = CallbackConfig(
url = "https://my-app.example.com/webhook/oid4vp",
statuses = listOf(
AuthorizationSessionStatus.AUTHORIZATION_RESPONSE_VERIFIED,
AuthorizationSessionStatus.ERROR
),
includeVerifiedData = true
)
),
sessionContext
)

Your webhook endpoint will receive a POST with a JSON body like this:

{
"correlation_id": "sess-abc123",
"status": "AUTHORIZATION_RESPONSE_VERIFIED",
"verified_data": {
"credentials": [...]
}
}

Dependencies

You need the universal implementation module plus the underlying verifier and DCQL libraries. If you're already using the standard verifier API, you only need to add the universal module.

dependencies {
// Universal OID4VP
implementation("com.sphereon.idk:lib-openid-oid4vp-universal-impl:0.25.0")

// Required dependencies
implementation("com.sphereon.idk:lib-openid-oid4vp-verifier-impl:0.25.0")
implementation("com.sphereon.idk:lib-openid-oid4vp-dcql:0.25.0")
}

Configuration

Instead of sending a full DCQL query with every request, you can register named queries in a registry. Then reference them by query_id when creating sessions. This keeps your request payloads small and your query definitions in one place.

@Inject
@SingleIn(AppScope::class)
class Oid4vpQueryRegistry {
val queries = mapOf(
"age-verification" to DcqlQuery(
credentials = listOf(
DcqlCredential(
id = "age_over_18",
format = "dc+sd-jwt",
claims = listOf(
DcqlClaim(path = listOf("age_over_18"))
)
)
)
),
"kyc-verification" to DcqlQuery(
credentials = listOf(
DcqlCredential(
id = "identity",
format = "dc+sd-jwt",
claims = listOf(
DcqlClaim(path = listOf("given_name")),
DcqlClaim(path = listOf("family_name")),
DcqlClaim(path = listOf("birth_date"))
)
)
)
)
)
}

Next Steps