Implement KYC verification
This guide shows you how to verify customer identity using Personal Identification Data (PID) credentials from an EUDI Wallet. You create a presentation request for government-issued identity attributes, display a QR code or deep link, and process verified data in your callback endpoint.
For a step-by-step learning experience that builds a complete project from scratch, see the KYC verification tutorial.
- A running connector instance with its public base URL configured and a callback URL pointing to your backend endpoint (for example,
http://localhost:3000/callback/kyc). See connector architecture for the deployment model and how callbacks are delivered. - Access to the internal management API (port 8081)
- A callback endpoint reachable from the connector over internal networking
- An X.509 access certificate configured in the connector
Overview
You request PID attributes (name, date of birth, address, nationalities) from an EUDI Wallet, verify them cryptographically, and deliver the result to your backend. This replaces document scans with government-issued digital credentials.
Time to implement: 2–4 hours.
Step 1: Create a presentation request
Call POST /oidc4vp on the management API with a DCQL query requesting PID attributes for identity verification.
The VCT values, requested claims, and credential values in this guide are examples. In production, request the attributes required by your use case and available in the target attestation scheme. Available attributes vary by issuer and member state.
- cURL
curl -X POST http://connector:8081/oidc4vp \
-H "Content-Type: application/json" \
-d '{
"dcql_query": {
"credentials": [
{
"id": "pid_kyc",
"format": "dc+sd-jwt",
"meta": {
"vct_values": ["urn:eudi:pid:1"]
},
"claims": [
{ "path": ["given_name"] },
{ "path": ["family_name"] },
{ "path": ["birthdate"] },
{ "path": ["address"] },
{ "path": ["nationalities"] }
]
}
]
},
"expires_in": 600
}'
Example response:
{
"state": "abc123",
"same_device_request_uri": "openid4vp://?client_id=x509_hash%3Aabc123def456&request_uri=https%3A%2F%2Fconnector.example.com%2Foidc4vp%2Fabc123%2Frequest%3Fflow_type%3Dsame-device",
"cross_device_request_uri": "openid4vp://?client_id=x509_hash%3Aabc123def456&request_uri=https%3A%2F%2Fconnector.example.com%2Foidc4vp%2Fabc123%2Frequest%3Fflow_type%3Dcross-device"
}
Store the state value to correlate with the callback later.
The response contains state for callback correlation, cross_device_request_uri for QR codes, and same_device_request_uri for deep links. Both URIs include a client_id derived from the connector's X.509 access certificate and a flow_type parameter (same-device or cross-device).
In the DCQL query, dc+sd-jwt specifies the SD-JWT credential format and urn:eudi:pid:1 identifies the PID credential type. The expires_in value is in seconds (600 = 10 minutes).
Step 2: Display the QR code or deep link
Use cross_device_request_uri to generate a QR code when the user is on a desktop. Use same_device_request_uri as a deep link when the user is on a mobile device.
This step is client-side. Use any QR code library (for example, qrcode for Node.js or com.google.zxing for Java) to encode the cross_device_request_uri into a QR code image. For same-device flows, redirect the user's browser to the same_device_request_uri deep link.
- Shell
# Cross-device: generate a QR code PNG from the URI (requires qrencode)
qrencode -o qrcode.png "$cross_device_request_uri"
# Same-device: open the deep link directly (macOS)
open "$same_device_request_uri"
Step 3: Implement the callback handler
Implement a callback endpoint that receives the PresentedCredentialsEvent from the connector. The state field correlates the event with your original request. For same-device flows, the responseCode field correlates the browser redirect with the callback.
The credentials and credentialsRaw fields may be absent for REJECTED, EXPIRED, PROCESSING_ERROR, and VERIFICATION_FAILED statuses. The errorDetails field is present only for REJECTED, PROCESSING_ERROR, and VERIFICATION_FAILED.
- cURL
# Simulate a FULFILLED callback for testing
curl -X POST http://localhost:3000/callback/kyc \
-H "Content-Type: application/json" \
-d '{
"status": "FULFILLED",
"state": "abc123",
"responseCode": "xyz789",
"credentials": {
"pid_kyc": [
{
"issuer": "https://issuer.example.com",
"claims": {
"given_name": "Erika",
"family_name": "Mustermann",
"birthdate": "1964-08-12",
"address": {"street_address": "Domplein 1", "house_number": "1", "postal_code": "90210", "country": "NL", "locality": "Utrecht", "region": "Utrecht"},
"nationalities": ["NL"]
},
"signatureIsValid": true,
"supportRevocation": false,
"supportTrustAnchor": true,
"isTrusted": false,
"kbKeyId": "KrXxHnYsf-abc123",
"kbSignatureIsValid": true
}
]
},
"credentialsRaw": {
"pid_kyc": [
{
"claims": "eyJ...",
"issuer": "https://issuer.example.com",
"kbKeyId": "KrXxHnYsf-abc123"
}
]
}
}'
The isRevoked field is only present when supportRevocation is true, and isTrusted is only present when supportTrustAnchor is true. A missing isRevoked does not mean "not revoked"—it means revocation checking is not supported for this credential. Always check the support* flags before reading the dependent fields.
The kbKeyId and kbSignatureIsValid fields are present when the credential includes device binding. Use kbKeyId to recognize returning users across sessions. The validFrom and validUntil fields are present when the credential includes iat and exp claims.
For the complete list of statuses and error codes, see the callback events reference.
Testing
Test checklist
- Presentation request creates and returns
state,same_device_request_uri, andcross_device_request_uri - QR code renders from
cross_device_request_uri - Deep link redirects to the wallet on mobile
- Callback receives
PresentedCredentialsEventwithFULFILLEDstatus - Callback correctly correlates
statewith the original session - Callback handles all five statuses (
FULFILLED,REJECTED,EXPIRED,PROCESSING_ERROR,VERIFICATION_FAILED) - Same-device flow correctly uses
responseCodefor browser redirect correlation - Expired sessions are handled gracefully
Troubleshooting
Callback not receiving events
The connector delivers callbacks synchronously with a default two-second timeout per attempt and one retry. Verify that your callback endpoint is reachable from the connector and responds within the timeout.
Missing credential fields
The fields isRevoked, isTrusted, kbKeyId, kbSignatureIsValid, validFrom, and validUntil are optional. Always check the support* flags before reading the dependent fields.
Session correlation failures
Verify that you store the state value from the POST /oidc4vp response and look it up when the callback arrives. The state value is a cryptographic random string generated by the connector.
Next steps
- Passwordless authentication—implement login with key binding proof
- Error handling—build robust status handling for all verification outcomes
- Going to production—production readiness checklist
Further reading
- KYC use case explained—conceptual overview of identity verification flows
- DCQL query language—how to specify credential requirements
- Device binding—how kbKeyId and device binding proof work
- Error codes—wallet-facing HTTP error responses
- Callback events—Presented Credentials Event statuses and payload fields