Developers · Payments · EUDI ARF TS12

Payment confirmation with the EUDI wallet: TS12 dynamic linking on your own tenant

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

The short answer

What
OpenID4VP requests with TS12 transaction data – payment, login or risky action, account information access, e-mandate – and the checks on the wallet’s answer.
Checked
The TS12 data model of the transaction, the hashes of exactly the entries sent, a one-time authentication code, two authentication factors, the response mode and the payment card itself (issuer, signature, revocation).
Result
The authentication code, the factors used, the transaction IDs and only the card attributes you asked for. Nobody is signed in and no account is created.
Where
Your CodeB tenant in the EU. The person needs a wallet that implements TS12 and a payment card (SCA attestation) from their bank.

The four TS12 transaction types

Every wallet that implements TS12 has to show and sign these four types. Your bank’s own type names can be mapped onto them.

TypeUsed forRequired fields
urn:eudi:sca:payment:1Payment confirmation: single, scheduled or recurring; account or card; also through a payment initiation servicetransaction_id, payee (name, id), currency, amount
urn:eudi:sca:login_risk_transaction:1Login and actions with a risk of fraud, for example raising a limittransaction_id, action
urn:eudi:sca:account_access:1Access to payment account information, also through an account information servicetransaction_id
urn:eudi:sca:emandate:1E-mandates, for example SEPA direct debittransaction_id, purpose (or payment_payload)

How it works

The same OpenID4VP flow as a wallet sign-in, with the transaction inside the signed request.

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.

Start the request

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 person confirms

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.

You get the result

Poll vp-response from the same browser. After all checks you get the authentication code, the factors and the transaction IDs.

The API

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.

Example

# 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"}]}}}

OpenAPI (platform) · OpenAPI (platform)

What the server checks

  • The payload against the TS12 data model of its type, including the rules in the text: no execution date in the past, no execution date together with a repetition, a mandate text when there is no payment, ISO 4217 currency, https links only, no hidden text-direction characters.
  • credential_ids name the payment-card query of the same request; only sha-256 is used.
  • The KB-JWT is signed with the key bound to the card, and its 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 card itself: issuer signature, trust chain, revocation status and the requested type, exactly as for every wallet presentation.
  • A payment confirmation is never produced from a session with weakened checks or from a card whose issuer chain could not be verified; the card must come as SD-JWT VC.
  • Requests without transaction data are not affected by any of these checks.

Try it

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.

Privacy by design

  • The wallet discloses only the card attributes you request; the transaction details stay between your request and the person’s confirmation.
  • A payment confirmation signs nobody in, creates no account and stores no identity data on the server.
  • The result goes only to the browser that started the request.
  • The log records that a confirmation happened and which checks passed, not amounts or payees.
  • Runs on your own CodeB tenant in the EU, without third-party processors.

Honest limits

  • The person needs a wallet that implements TS12 – showing the transaction and adding the hashes, jti and amr to its answer. A wallet without TS12 support refuses the request.
  • TS12 v1.0.1 covers SD-JWT VC; presentations in other formats are not accepted for transaction data.
  • The request is signed; it is not encrypted yet (TS12 recommends encryption for requests with personal data).
  • CodeB delivers the authentication result. Executing the payment over a payment network stays with your bank or payment service; CodeB claims no PSD2 certification.
  • The test card on phone.codeb.io is for tests only. Real payment cards (SCA attestations) come from the person’s bank.

Frequently asked questions

What is TS12?

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.

What is dynamic linking?

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.

What is the authentication code?

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.

Which wallets work?

Any wallet built to the EUDI ARF that implements TS12 and holds the payment card (SCA attestation) your request asks for.

Can my bank use its own transaction types?

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.

Does CodeB move money?

No. CodeB checks the confirmation and gives your backend the result; the payment itself runs through your bank or payment service.

Start with one test transaction

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.