Skip to main content

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.

Prerequisites

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 -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 include bankName, accountHolder, accountNumber, iban, bic, currency, accountType, sub, and userId.
  • 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—an openid-credential-offer:// URI for the wallet. Display this to the customer as a QR code or deep link.
  • tx_code_value (conditional)—present only when tx_code is 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).

# 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.

# 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:

FieldDescription
eventIdCorrelation key tied to the offer lifecycle. Set to the same value as offerId. To deduplicate retries, combine eventId with status.
statusOne of OFFER_CREATED, ISSUED, FAILED, or EXPIRED.
offerIdCorrelation token matching the offer_id from Step 1.
errorDetailsPresent 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 errorDetails for 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_id and credential_offer_uri
  • Response status is HTTP 201 Created
  • offer_id is 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_CREATED status immediately after offer creation
  • Callback receives the issuance event with ISSUED status after wallet completes
  • Callback correctly correlates offerId with the customer session and updates account status
  • Callback handles all four statuses (OFFER_CREATED, ISSUED, FAILED, EXPIRED)
  • FAILED callback includes errorDetails and logs the failure
  • Expired sessions are handled gracefully with an option to retry
  • Account status reflects the issuance outcome (credential-enabled, failed, or expired)
  • eventId matches offerId in 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

Further reading