Implement AOC issuance
This guide shows you how to issue an Account Ownership Credential (AOC) to an EUDI Wallet as part of a bank account opening flow. You create a credential offer with AOC-specific claims derived from the customer's account session, display a QR code or deep link, and process the issuance callback to update the account status.
For a step-by-step learning experience that builds a complete project from scratch, see the AOC issuance tutorial. For a conceptual overview, see AOC issuance use case.
- 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/issuance). 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
- An Issuer Signing Certificate configured in the connector. See Manage certificates to generate one.
- Type Metadata configured for the AOC credential type
- Familiarity with the generic credential issuance how-to
Overview
You build a production-ready AOC issuance flow for a banking app. After a customer completes account opening and identity verification, your backend creates a credential offer with claims derived from the account session, displays the offer to the customer, and handles the callback to mark the account as credential-enabled.
This guide differs from the generic credential issuance how-to by covering production integration concerns specific to AOC: correlating offers to authenticated bank customers, deriving AOC claims from the account opening session, and handling the issuance callback to update account status.
Time to implement: 2–4 hours.
Step 1: Create an AOC credential offer
After the customer completes account opening and identity verification (KYC), create a credential offer using claims from the authenticated session. Call POST /offers on the management API with the AOC credential configuration ID and claims derived from the account opening session. The connector returns 201 Created on success.
Correlate the offer_id in the response with the authenticated bank customer so you can update the correct account when the callback arrives.
- cURL
curl -X POST http://connector:8081/offers \
-H "Content-Type: application/json" \
-d '{
"credential_configuration_id": "AccountOwnershipCredential",
"claims": {
"bankName": "Example Bank",
"accountHolder": "Erika Mustermann",
"accountNumber": "1234567890",
"iban": "DE99370501981234567890",
"bic": "COLSDE33XXX",
"currency": "EUR",
"accountType": "checking",
"sub": "user-uuid-123",
"userId": "user-uuid-123"
}
}'
Example response (201 Created):
{
"offer_id": "abc123def456",
"credential_offer_uri": "openid-credential-offer://?credential_offer_uri=https%3A%2F%2Fissuer.example.com%2Foidc4vci%2Foffers%2Fabc123def456"
}
The request body contains:
credential_configuration_id—references the AOC credential configuration in the connector's Credential Issuer Metadata. Must match a configured type in your Type Metadata.claims—AOC-specific claims derived from the account opening session. These includebankName,accountHolder,accountNumber,iban,bic,currency,accountType,sub, anduserId.tx_code(optional)—adds transaction code authorization. See Use transaction codes for details.
The response contains:
offer_id—a correlation token for matching callbacks to this offer. Store this alongside the customer's account ID so you can update the correct account when the callback arrives.credential_offer_uri—anopenid-credential-offer://URI for the wallet. Display this to the customer as a QR code or deep link.tx_code_value(conditional)—present only whentx_codeis included in the request. Deliver this to the customer via a separate channel (for example, SMS or email).
Step 2: Display the credential offer
Present the credential_offer_uri to the customer after account opening completes. Use a QR code for cross-device flows (customer is on a desktop) or a deep link for same-device flows (customer is on a mobile device).
- Shell
# Cross-device: generate a QR code PNG from the URI (requires qrencode)
qrencode -o qrcode.png "$credential_offer_uri"
# Same-device: open the deep link directly (macOS)
open "$credential_offer_uri"
Step 3: Implement the callback handler
Implement a callback endpoint that receives the issuance event from the connector. Use the offerId field to look up the customer session you stored in Step 1 and update the account status based on the issuance outcome.
- cURL
# Simulate an ISSUED callback for testing
curl -X POST https://example.com:3000/callback/issuance \
-H "Content-Type: application/json" \
-d '{
"eventId": "abc123def456",
"status": "ISSUED",
"offerId": "abc123def456"
}'
The callback payload contains:
| Field | Description |
|---|---|
eventId | Correlation key tied to the offer lifecycle. Set to the same value as offerId. To deduplicate retries, combine eventId with status. |
status | One of OFFER_CREATED, ISSUED, FAILED, or EXPIRED. |
offerId | Correlation token matching the offer_id from Step 1. |
errorDetails | Present for FAILED status. Describes the failure reason. |
Handle each status:
- OFFER_CREATED—the offer was created and the session is active. Use for audit logging or to start a timeout timer.
- ISSUED—the AOC was successfully issued to the customer's wallet. Update the account status to reflect that the credential is active. The customer can now use the AOC for passwordless authentication.
- FAILED—issuance failed. Check
errorDetailsfor the reason, log the error, and consider offering the customer a retry from the account dashboard. - EXPIRED—the session time-to-live (TTL) expired before the wallet completed the flow. Create a new offer if the customer wants to try again.
For the complete list of payload fields and status descriptions, see the callback events reference.
Testing
Test checklist
- Credential offer creates with AOC-specific claims and returns
offer_idandcredential_offer_uri - Response status is HTTP
201 Created -
offer_idis correctly correlated with the customer's account session - QR code renders from
credential_offer_uri - Deep link opens the wallet on mobile
- Callback receives
OFFER_CREATEDstatus immediately after offer creation - Callback receives the issuance event with
ISSUEDstatus after wallet completes - Callback correctly correlates
offerIdwith the customer session and updates account status - Callback handles all four statuses (
OFFER_CREATED,ISSUED,FAILED,EXPIRED) -
FAILEDcallback includeserrorDetailsand logs the failure - Expired sessions are handled gracefully with an option to retry
- Account status reflects the issuance outcome (credential-enabled, failed, or expired)
-
eventIdmatchesofferIdin all callbacks
Troubleshooting
Session correlation failures
Verify that you store the offer_id from the POST /offers response alongside the customer's account ID and look it up when the callback arrives. The offerId in the callback matches the offer_id from the original offer response.
Offer expiration before wallet completes
The default session time-to-live (TTL) is five minutes. If customers consistently time out during account opening, consider creating the credential offer only after the customer confirms they are ready to scan, or guide them to scan the QR code promptly.
unknown_credential_configuration error
The credential_configuration_id in your offer request doesn't match any configured credential type. Verify that your Type Metadata is configured for the AOC credential type and that the credential_configuration_id matches a key in the connector's Credential Issuer Metadata.
Next steps
- Issue a credential—underlying API mechanics for credential issuance
- Use transaction codes—add transaction code authorization to issuance flows
- Handle errors—build robust status handling for all issuance outcomes
Further reading
- AOC issuance use case—conceptual overview of why banks issue AOCs
- OID4VCI protocol—how the issuance protocol works
- Callback events—issuance event statuses and payload fields
- Passwordless authentication—use the issued AOC for login