Skip to main content
Version: v0.13

OID4VP Integration Guide

This guide walks you through integrating the Universal OID4VP API into your application to verify credentials from digital wallets.

Prerequisites

Before starting, ensure you have:

  1. API Access - OAuth 2.0 client credentials for the Universal OID4VP API
  2. Query ID - A pre-configured query_id from your administrator
  3. HTTPS Endpoint - Your backend must be accessible via HTTPS for callbacks

Integration Steps

Step 1: Get Your Query ID

Contact your administrator to obtain the query_id for your use case. Common examples:

Query IDDescription
age_verificationVerify user is over 18
identity_checkFull identity verification
license_verificationProfessional license check

The administrator configures which credentials and claims each query requests.

Step 2: Create Authorization Request

When a user needs to verify their credentials:

interface CreateRequestOptions {
queryId: string;
correlationId?: string;
callbackUrl?: string;
}

async function createVerificationRequest(options: CreateRequestOptions) {
const response = await fetch(`${API_BASE}/oid4vp/backend/auth/requests`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${await getAccessToken()}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
query_id: options.queryId,
correlation_id: options.correlationId || crypto.randomUUID(),
qr_code: {
size: 300,
color_dark: '#1a1a1a'
},
callback: options.callbackUrl ? {
url: options.callbackUrl,
statuses: ['authorization_response_verified', 'error'],
include_verified_data: true
} : undefined
})
});

if (!response.ok) {
throw new Error(`Failed to create request: ${response.statusText}`);
}

return response.json();
}

Step 3: Display QR Code to User

Present the QR code to the user for wallet scanning:

<!-- Frontend: Display QR code -->
<div id="verification-container">
<h3>Scan with your wallet</h3>
<img id="qr-code" alt="Scan to verify" />
<p id="status">Waiting for wallet...</p>

<!-- Same-device flow: deep link button -->
<a id="open-wallet" class="button" style="display: none;">
Open Wallet
</a>
</div>

<script>
async function startVerification() {
const session = await createVerificationRequest({
queryId: 'age_verification',
correlationId: orderId
});

// Display QR code
document.getElementById('qr-code').src = session.qr_code_data_uri;

// Show deep link for same-device flow
const openWalletLink = document.getElementById('open-wallet');
openWalletLink.href = session.request_uri;
openWalletLink.style.display = 'inline-block';

// Start polling for status
pollForCompletion(session.correlation_id);
}
</script>

Step 4: Poll for Completion

Poll the status endpoint until verification completes:

interface VerificationResult {
success: boolean;
claims?: Record<string, any>;
error?: string;
}

async function pollForCompletion(
correlationId: string,
maxAttempts = 60,
intervalMs = 2000
): Promise<VerificationResult> {
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const response = await fetch(
`${API_BASE}/oid4vp/backend/auth/requests/${correlationId}`,
{
headers: {
'Authorization': `Bearer ${await getAccessToken()}`
}
}
);

const status = await response.json();

switch (status.status) {
case 'authorization_response_verified':
return {
success: true,
claims: status.verified_data?.credentials?.[0]?.claims
};

case 'error':
return {
success: false,
error: status.error?.description || 'Verification failed'
};

case 'authorization_request_retrieved':
// User scanned QR, update UI
updateStatus('User is selecting credentials...');
break;
}

await new Promise(resolve => setTimeout(resolve, intervalMs));
}

return { success: false, error: 'Verification timed out' };
}

Step 5: Handle Webhook Callbacks (Optional)

For server-to-server notification, implement a webhook handler:

import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.serialization.Serializable

@Serializable
data class Oid4vpCallback(
val correlationId: String,
val status: String,
val queryId: String?,
val verifiedData: VerifiedData? = null,
val error: CallbackError? = null
)

@Serializable
data class VerifiedData(val credentials: List<VerifiedCredential>)

@Serializable
data class VerifiedCredential(
val id: String,
val format: String,
val type: String?,
val claims: Map<String, JsonElement>
)

@Serializable
data class CallbackError(
val code: String,
val description: String?
)

fun Application.configureWebhooks() {
routing {
post("/webhooks/oid4vp") {
val callback = call.receive<Oid4vpCallback>()

when (callback.status) {
"authorization_response_verified" -> {
val claims = callback.verifiedData
?.credentials
?.firstOrNull()
?.claims

// Process verified claims
processVerifiedClaims(callback.correlationId, claims)
}
"error" -> {
handleVerificationError(
callback.correlationId,
callback.error?.description
)
}
}

call.respond(HttpStatusCode.OK)
}
}
}

private suspend fun processVerifiedClaims(
correlationId: String,
claims: Map<String, JsonElement>?
) {
// Update your database with verified data
val ageOver18 = claims?.get("age_over_18")
?.jsonPrimitive
?.booleanOrNull

if (ageOver18 == true) {
orderService.approveOrder(correlationId)
} else {
orderService.rejectOrder(correlationId, "Age verification failed")
}
}

Step 6: Clean Up Sessions

After verification completes or times out, clean up the session:

async function cleanupSession(correlationId: string): Promise<void> {
await fetch(
`${API_BASE}/oid4vp/backend/auth/requests/${correlationId}`,
{
method: 'DELETE',
headers: {
'Authorization': `Bearer ${await getAccessToken()}`
}
}
);
}

Complete Flow Example

Here's a complete React component demonstrating the full flow:

import React, { useState, useEffect } from 'react';

interface VerificationProps {
queryId: string;
orderId: string;
onSuccess: (claims: Record<string, any>) => void;
onError: (error: string) => void;
}

export function CredentialVerification({
queryId,
orderId,
onSuccess,
onError
}: VerificationProps) {
const [qrCode, setQrCode] = useState<string | null>(null);
const [deepLink, setDeepLink] = useState<string | null>(null);
const [status, setStatus] = useState<string>('initializing');
const [correlationId, setCorrelationId] = useState<string | null>(null);

// Create session on mount
useEffect(() => {
async function initSession() {
try {
const session = await createVerificationRequest({
queryId,
correlationId: orderId
});

setQrCode(session.qr_code_data_uri);
setDeepLink(session.request_uri);
setCorrelationId(session.correlation_id);
setStatus('waiting');
} catch (error) {
onError('Failed to start verification');
}
}

initSession();
}, [queryId, orderId]);

// Poll for completion
useEffect(() => {
if (!correlationId || status !== 'waiting') return;

const poll = async () => {
try {
const result = await pollForCompletion(correlationId);

if (result.success) {
setStatus('success');
onSuccess(result.claims!);
} else {
setStatus('error');
onError(result.error!);
}
} catch (error) {
setStatus('error');
onError('Polling failed');
}
};

poll();
}, [correlationId, status]);

// Cleanup on unmount
useEffect(() => {
return () => {
if (correlationId) {
cleanupSession(correlationId).catch(console.error);
}
};
}, [correlationId]);

return (
<div className="verification-container">
{status === 'initializing' && <p>Starting verification...</p>}

{status === 'waiting' && (
<>
<h3>Verify Your Credentials</h3>
{qrCode && (
<img
src={qrCode}
alt="Scan with your wallet"
className="qr-code"
/>
)}
<p>Scan with your wallet app</p>

{deepLink && (
<a href={deepLink} className="button">
Open Wallet
</a>
)}
</>
)}

{status === 'success' && (
<div className="success">Verification successful</div>
)}

{status === 'error' && (
<div className="error">Verification failed</div>
)}
</div>
);
}

Best Practices

Security

  1. Always use HTTPS for your callback URLs
  2. Validate webhook signatures if your implementation supports them
  3. Use short session TTLs (10 minutes is recommended)
  4. Clean up sessions after completion to prevent replay attacks

User Experience

  1. Show status updates as the wallet interacts with the request
  2. Provide a deep link for same-device flows
  3. Handle timeouts gracefully with clear messaging
  4. Allow retry if verification fails

Performance

  1. Use webhooks instead of polling when possible
  2. Cache access tokens to avoid repeated OAuth flows

Troubleshooting

Common Issues

IssueCauseSolution
QR code not scanningWrong URL schemeEnsure wallet supports openid4vp://
Wallet shows errorInvalid query configurationContact administrator
TimeoutUser abandoned flowImplement retry mechanism
No verified dataWrong status checkPoll until authorization_response_verified

Debug Mode

Enable verbose logging to diagnose issues:

// In your application configuration
sphereon.oid4vp.debug = true

Next Steps