Build the transaction
Your backend builds one transaction_data entry: type, credential_ids ["ts12_sca"], transaction_data_hashes_alg ["sha-256"] and the TS12 payload.
Developers · Payments · EUDI ARF TS12
eIDAS 2 (Article 5f) requires banks and payment service providers to accept the EUDI wallet for strong customer authentication when the customer asks for it. Technical Specification 12 (TS12) of the EUDI ARF describes how: the request carries the transaction, the wallet shows it and signs it with the key of the customer’s payment card, and the bank checks that exactly this transaction was confirmed. CodeB runs these checks on your own tenant.
OpenID4VP 1.0 · SD-JWT VC · EUDI ARF TS12 v1.0.1 · EU-hosted, your own tenant
Every wallet that implements TS12 has to show and sign these four types. Your bank’s own type names can be mapped onto them.
| Type | Used for | Required fields |
|---|---|---|
urn:eudi:sca:payment:1 | Payment confirmation: single, scheduled or recurring; account or card; also through a payment initiation service | transaction_id, payee (name, id), currency, amount |
urn:eudi:sca:login_risk_transaction:1 | Login and actions with a risk of fraud, for example raising a limit | transaction_id, action |
urn:eudi:sca:account_access:1 | Access to payment account information, also through an account information service | transaction_id |
urn:eudi:sca:emandate:1 | E-mandates, for example SEPA direct debit | transaction_id, purpose (or payment_payload) |
The same OpenID4VP flow as a wallet sign-in, with the transaction inside the signed request.
Your backend builds one transaction_data entry: type, credential_ids ["ts12_sca"], transaction_data_hashes_alg ["sha-256"] and the TS12 payload.
vp-start with query=sca, the card type and the entries. CodeB checks the payload and answers with a QR code link and a same-phone link to a signed request.
The wallet shows payee, amount and the other fields and signs with the card’s key: the KB-JWT carries the transaction hashes, a fresh jti, the factors used (amr) and the response_mode.
Poll vp-response from the same browser. After all checks you get the authentication code, the factors and the transaction IDs.
GET /oidc.ashx?action=vp-start&query=scaParameters: vct (type of the SCA attestation, https URL or urn), claims (card attributes to disclose, comma separated, at most 10), transaction_data (base64url of a JSON array of base64url entries, 1–16 entries). Answer {id, request_uri, deep_link}. Errors: 400 invalid_request "transaction_data invalid: ts12_<reason>".GET /oidc.ashx?action=vp-response&id=<id>Poll from the browser that started the request (same-device cookie). When confirmed: status: completed, claims (requested card attributes, vct, test_credential) and transaction_data (verified, hashes, transaction_ids, sca.authentication_code, sca.amr). Unconfirmed: 403 transaction_data_not_confirmed.GET /vc/<id>/<version>SD-JWT VC type metadata of a card type issued by this tenant: category urn:eu:europa:ec:eudi:sua:sca and transaction_data_types with labels, display levels and UI labels for the wallet.Bank-specific typesA transaction type URI of your own can be mapped onto one of the four TS12 data models per tenant; requests with unknown types are refused.# 1. One transaction_data entry per transaction (TS12 payload), base64url-encoded
{
"type": "urn:eudi:sca:payment:1",
"credential_ids": ["ts12_sca"],
"transaction_data_hashes_alg": ["sha-256"],
"payload": {
"transaction_id": "8D8AC610-566D-4EF0-9C22-186B2A5ED793",
"payee": {"name": "Demo Shop", "id": "DE02120300000000202051"},
"currency": "EUR",
"amount": 12.50
}
}
# 2. Start the request (transaction_data = base64url of the JSON array of the base64url entries)
GET /oidc.ashx?action=vp-start&query=sca&vct=<SCA attestation type>&claims=iban&transaction_data=<...>
-> {"id", "request_uri", "deep_link"} (QR code or same-phone link)
# 3. After the person confirmed in the wallet: poll from the same browser
GET /oidc.ashx?action=vp-response&id=<id>
-> {"status": "completed",
"claims": {"iban": "...", "vct": "..."},
"transaction_data": {"verified": true, "hashes_alg": "sha-256", "hashes": ["..."],
"transaction_ids": ["8D8AC610-..."],
"sca": {"authentication_code": "<KB-JWT jti>", "amr": [{"knowledge": "pin_6_or_more_digits"}, {"possession": "key_in_local_native_wscd"}]}}}
credential_ids name the payment-card query of the same request; only sha-256 is used.transaction_data_hashes are the SHA-256 hashes of exactly the entries sent – nothing more, nothing less.jti is accepted once (also across a cluster) and becomes the authentication code; amr names at least two different factor categories (knowledge, possession, inherence) with the values TS12 defines; response_mode equals the request’s.The test page plays the bank or shop: choose a transaction type, confirm with the wallet and see the expected hash, the authentication code and the factors. On phone.codeb.io a test payment card (type https://phone.codeb.io/vc/sca-test-account/1.0) can be added to a wallet; it is not a bank account and no money moves.
jti and amr to its answer. A wallet without TS12 support refuses the request.Technical Specification 12 of the EUDI Architecture and Reference Framework (ARF): how strong customer authentication for payments under PSD2 works with the EUDI wallet. It defines the transaction data in the request, how the wallet shows it and what the wallet signs.
PSD2 requires that the authentication is bound to a specific amount and payee. With TS12 the wallet signs a hash of exactly the transaction it showed, so a changed amount or payee no longer matches.
The jti of the wallet’s key-binding JWT: a fresh random value per confirmation. CodeB accepts each value once and returns it as the authentication code.
Any wallet built to the EUDI ARF that implements TS12 and holds the payment card (SCA attestation) your request asks for.
Yes. A type URI of your own is mapped onto one of the four TS12 data models per tenant, so the server checks it like the built-in types.
No. CodeB checks the confirmation and gives your backend the result; the payment itself runs through your bank or payment service.
Open the test page, add the test payment card to a wallet that implements TS12 and confirm a payment. Then build the same request in your backend.