Credentials — field reference¶
Status: 23 October 2026, contract v3. Every credential in the Scoped Mandate PoC, field by field: what each field means, who sets it, who checks it. The field tables on this page are machine-checked: the bank smokes compare them with credentials that bank and operator actually issue, and fail when a field is missing here or documented but no longer issued (see Keeping this page current).
Overview¶
| Credential | Type (vct) |
Issuer | Bound to (cnf.jwk) |
Issued via | Presented to |
|---|---|---|---|---|---|
| Konto Karte | urn:digitallabor:sparkasse:sca:1 |
the bank, https://bank.s-you.me |
the customer's wallet key (Secure Enclave, Face ID) | OpenID4VCI authorization code after online-banking login | the bank (mandate approval, PaSO) and the operator (agent onboarding) |
| Agent credential | urn:digitallabor:agent:1 |
the operator, https://operator.s-you.me |
the agent key (Secure Enclave, no biometry) | OpenID4VCI pre-authorized code after the customer identified with the Konto Karte | the bank (mandate request) and the merchant (purchase) |
| Mandate credential | urn:digitallabor:sparkasse:mandate:1 |
the bank, https://bank.s-you.me |
the agent key — the same key as the agent credential | OpenID4VCI pre-authorized code from the approved PaSO session | the merchant (purchase) |
Two further signed objects belong to the picture but are not credentials: the checkout part, which the mandate credential commits to by hash, and the status list, through which issuers revoke. How a holder proves possession of its key when a credential is issued is in proof JWT. Since 2026-10-25 the bank also signs every executed payment: the payment confirmation is the token with which the agent completes a UCP checkout.
Common format¶
All three credentials are SD-JWT VCs (dc+sd-jwt): <issuer JWT>~<disclosure>~…~ and, when presented, a key-binding JWT appended at the end.
- The issuer JWT is signed with ES256 by the issuer's certificate; the header carries the chain (
x5c) to the issuer's root (/issuer/root.pemat bank and operator). - Claims marked selectively disclosable are not in the payload. The payload holds only their digests in
_sd; the values travel as separate disclosures[salt, name, value], and the holder decides which disclosures to present. - Times without a date format are Unix seconds (
iat,nbf,exp); dates in claims are ISO 8601 UTC. Amounts are integer cents. - Issuers keep a readable copy of every credential they issued since 23 October 2026 — header, payload and disclosed claims, without signature and salts — behind Details in the bank dashboard and the operator portal (
GET /issuer/credentials/{id}).
The header is the same for all three credentials:
| Field | Presence | Example | Meaning |
|---|---|---|---|
alg |
always | ES256 |
Signature algorithm of the issuer JWT. |
typ |
always | dc+sd-jwt |
Media type: an SD-JWT VC. |
kid |
always | issuer-1 |
Key id of the issuer's signing key. |
x5c |
always | [leaf, root] |
Certificate chain, leaf first. Verifiers check the chain to the issuer's root and the issuer signature with the leaf key; the bank also requires the leaf's DNS name to match the host of iss for foreign issuers. |
1. Konto Karte¶
The customer's bank card in the wallet: identity and login at the bank. With it the customer approves mandates (strong customer authentication in the PaSO profile) and identifies to the agent's operator.
| Type | urn:digitallabor:sparkasse:sca:1, configuration sparkasse_sca |
| Issued | by the bank after login in online banking (OpenID4VCI authorization code with PAR and PKCE) |
| Lifetime | 365 days (CREDENTIAL_TTL_DAYS) |
| Revocation | bank status list; bank dashboard → Credentials → widerrufen |
| Card in the wallet | "Konto Karte", issuer line "Sparkasse Agentenstadt", red |
Payload¶
| Field | Presence | Example | Meaning | Checked by |
|---|---|---|---|---|
iss |
always | https://bank.s-you.me |
Issuer. | bank, operator: on the trust list, x5c leaf bound to this host |
vct |
always | urn:digitallabor:sparkasse:sca:1 |
Credential type. | bank, operator: must be an accepted SCA type |
iat |
always | 1792704125 |
Issued at. | — |
nbf |
always | 1792704065 |
Valid from; iat minus 60 s against clock skew. |
— |
exp |
always | 1824240125 |
Valid until. | bank, operator |
issuing_bank |
always | Sparkasse Agentenstadt |
Display name of the bank (display only; trust comes from iss). |
— |
cnf.jwk |
always | {kty: EC, crv: P-256, x, y} |
The customer's wallet key. Only this key can sign the key-binding JWT of a presentation. | bank, operator: key binding |
status.status_list.uri |
always | https://bank.s-you.me/issuer/status/1 |
Status list the credential is registered in. | bank, operator |
status.status_list.idx |
always | 193 |
Bit index in that list; 1 = revoked. | bank, operator |
_sd |
always | ["3A5K…", …] |
Digests of the selectively disclosable claims below. | verifiers match each presented disclosure |
_sd_alg |
always | sha-256 |
Digest algorithm for _sd. |
— |
Selectively disclosable¶
| Field | Presence | Example | Meaning | Who asks for it |
|---|---|---|---|---|
given_name |
always | Yuki |
First name. | operator (onboarding: who onboards the agent) |
family_name |
always | Tanaka |
Last name. | operator |
iban_masked |
always | DE89 **** **** **** 3000 |
Account, masked. | — |
customer_id |
always | cust_yuki_0001 |
The bank's customer reference. | bank (mandate approval: becomes principal of the mandate credential); operator |
sca_level |
always | sca |
Assurance of the login behind the card. | — |
Presentation¶
The wallet appends a key-binding JWT (typ: kb+jwt), signed with the key in cnf.jwk, after Face ID.
| To | Required claims in the key-binding JWT |
|---|---|
| the bank, mandate approval (PaSO, §1.14) | see the table below; the bank publishes the required set as kb_jwt_claims_required at /paso/.well-known |
| the operator, agent onboarding (§1.16) | nonce, aud, iat, sd_hash |
Key-binding JWT to the bank for the mandate approval (PaSO Core §6.1 plus key binding):
| Field | Presence | Example | Meaning |
|---|---|---|---|
nonce |
always | n-0S6_WzA2Mj |
Nonce of the bank's request (JAR); single use. |
aud |
always | x509_san_dns:bank.s-you.me |
The bank's client id. |
iat |
always | 1792704137 |
Signed at; at most 5 minutes old. |
sd_hash |
always | Kb3… |
Hash over the presented SD-JWT without the key-binding JWT. |
transaction_data_hashes |
always | ["q9X…"] |
SHA-256 of the transmitted transaction_data entry — the mandate the customer saw (payment part and hash C). Dynamic linking. |
transaction_data_hashes_alg |
optional | sha-256 |
Algorithm for the line above. |
jti |
always | 6a1f… |
Unique id of this approval; replay protection. |
response_mode |
always | direct_post.jwt |
How the wallet answered (encrypted). |
display_locale |
always | de-DE |
Language the mandate was shown in. |
amr |
always | ["hwk", "bio_strong", "face"] |
Authentication methods; becomes sca.amr of the mandate credential. |
transaction_data_hash |
always | q9X… |
PaSO form of the hash above (single value). |
transaction_data_hash_alg |
always | sha-256 |
Its algorithm. |
request_integrity |
always | sha256-… |
Hash of the request (JAR) the wallet displayed; proves the wallet saw the bank's exact request. |
wallet_instance_version |
always | ios:793:2.6.0 |
Wallet build. |
metadata_integrity |
optional | sha256-… |
Hash of the verifier metadata, if the wallet sends it. |
2. Agent credential¶
The shopping agent's identity, issued by its operator. It ties the agent key to a legal person that is accountable for the agent. In the target picture it comes from the operator's European Business Wallet.
| Type | urn:digitallabor:agent:1, configuration agent_identity |
| Issued | by the operator after the customer presented the Konto Karte there (OpenID4VCI pre-authorized code, proof signed by the agent key) |
| Lifetime | 365 days |
| Revocation | operator status list; operator portal → widerrufen |
| In the app | the green agent card "EUDIPal Einkäufer" |
Payload¶
| Field | Presence | Example | Meaning | Checked by |
|---|---|---|---|---|
iss |
always | https://operator.s-you.me |
Issuer: the operator. | bank (on AGENT_TRUSTED_ISSUERS), merchant (chain to the operator root) |
vct |
always | urn:digitallabor:agent:1 |
Credential type. | bank, merchant |
iat |
always | 1792704124 |
Issued at. | — |
nbf |
always | 1792704064 |
Valid from. | — |
exp |
always | 1824240124 |
Valid until. | bank, merchant |
agent_id |
always | agt_0db3f2b42011cdd0 |
Agent id: agt_ + first 16 hex of SHA-256 over the agent's public key (SEC1, 0x04‖X‖Y). Derived from the key, so identity and key cannot drift apart. |
bank: equals the short form in the mandate request |
issuer_role |
always | operator |
Role of the issuer. | — |
cnf.jwk |
always | {kty: EC, crv: P-256, x, y} |
The agent key. The same key appears in the mandate credential. | bank: equals agent_public_key_jwk; merchant: equals the mandate credential's cnf |
status.status_list.uri |
always | https://operator.s-you.me/issuer/status/1 |
Operator status list. | bank, merchant |
status.status_list.idx |
always | 88 |
Bit index. | bank, merchant |
_sd |
always | ["Xnb3…", …] |
Digests of the disclosable claims below. | — |
_sd_alg |
always | sha-256 |
Digest algorithm. | — |
Selectively disclosable¶
| Field | Presence | Example | Meaning | Who asks for it |
|---|---|---|---|---|
agent_display_name |
always | EUDIPal Einkäufer |
Name of the agent. | bank, merchant |
operator_legal_person |
always | digitallabor.berlin GmbH |
The operator as a legal person. | bank, merchant |
operator_register_id |
always | HRB 000000 B (Mock) |
Register number of the operator. | merchant |
The operator also records who onboarded the agent (the Konto Karte's name and customer reference). That stays in the operator's register; it is not in the credential.
Presentation¶
The agent signs the key-binding JWT with the agent key; no Face ID.
| To | Required claims in the key-binding JWT |
|---|---|
the bank, mandate request (agent_presentation, §1.16) |
nonce (from POST /agent-nonces, single use), aud, iat (max. 5 min old), sd_hash; jti is recorded if present |
| the merchant, purchase (§2.5) | nonce, aud = merchant id, iat, sd_hash, transaction_data_hashes (hash of the basket entry), transaction_data_hashes_alg; jti is recorded if present |
3. Mandate credential¶
The approved mandate itself, issued by the bank to the agent key (contract v3). It carries the payment part in the clear and commits to the checkout part by hash. The bank keeps the mandate; this credential is its portable proof.
| Type | urn:digitallabor:sparkasse:mandate:1, configuration scoped_mandate |
| Issued | by the bank when the PaSO session of the approval turns authorized: the session result carries a pre-authorized-code offer, the agent redeems it with a proof signed by the agent key. One credential per mandate; the proof key must equal the mandate's agent_public_key_jwk |
| Lifetime | until the mandate's payment_scope.valid_until |
| Revocation | bank status list; set by POST /mandates/{id}/revoke, the SPAA cancel and the dashboard |
| In the app | not a card of its own; shown in the agent card's mandate list ("Mandats-Credential: von bank.s-you.me · Status-Bit …") |
Payload¶
| Field | Presence | Example | Meaning | Checked by |
|---|---|---|---|---|
iss |
always | https://bank.s-you.me |
Issuer: the bank. | merchant: chain to the bank root, iss = the bank |
vct |
always | urn:digitallabor:sparkasse:mandate:1 |
Credential type. | merchant |
iat |
always | 1792704139 |
Issued at. | — |
nbf |
always | 1792704079 |
Valid from. | merchant |
exp |
always | 1795296125 |
Valid until; equals the end of the mandate. | merchant |
issuing_bank |
always | Sparkasse Agentenstadt |
Display name of the bank. | — |
mandate_id |
always | mnd_b5cada6da34943c9 |
The mandate at the bank. Payment orders reference it. | merchant: equals the mandate of the checkout session |
cnf.jwk |
always | {kty: EC, crv: P-256, x, y} |
The agent key, the same as in the agent credential. | merchant: equals the agent credential's cnf |
status.status_list.uri |
always | https://bank.s-you.me/issuer/status/1 |
Bank status list. | merchant |
status.status_list.idx |
always | 198 |
Bit index; the bank sets it when the mandate is revoked. | merchant: bit set → checkout_mandate_revoked |
payment_scope.max_per_transaction |
always | 6000 |
Limit per purchase in cents (60 €). | the bank at payment (limit_exceeded) |
payment_scope.max_total |
always | 30000 |
Total limit in cents (300 €). | the bank at payment (total_exceeded) |
payment_scope.currency |
always | EUR |
Currency. | the bank at payment |
payment_scope.valid_from |
always | 2026-10-22T21:21:05Z |
Start of the mandate. | merchant, bank |
payment_scope.valid_until |
always | 2026-11-21T21:22:05Z |
End of the mandate. | merchant, bank |
payment_scope.merchant_whitelist |
always | [] |
Named merchants at which the agent may pay. May be empty (since 2026-10-24, like AP2 merchants): then any merchant of a kind in merchant_categories. Mandates from builds ≤ 796: ["merchant_kiezmarkt"]. |
merchant: if non-empty, itself on the list, else mandate_not_verified; bank at payment (merchant_not_allowed) |
payment_scope.merchant_categories |
always | ["5411"] |
Merchant category codes (ISO 18245; 5411 = grocery stores, 5912 = drugstores). The kind of merchant, not product categories; from the SPAA Dynamic Recurring Payment (creditorCategoryCodes). At least one of merchant_whitelist and merchant_categories is non-empty. |
the bank at payment, against the MCC in its own merchant directory (merchant_category_not_allowed) — not the merchant |
payment_scope.scheme |
always | spaa_drp |
The mandate is an SPAA Dynamic Recurring Payment. | — |
payment_scope.rail |
always | sct_inst |
Every payment settles as SEPA Instant Credit Transfer. | — |
checkout_hash |
always | sha256:92c20e5c… |
Hash C — SHA-256 over the canonical JSON of the checkout part (product categories, age rule). The bank only knows this hash; the merchant recomputes it over the checkout part it receives in the clear. | merchant: mismatch → checkout_mandate_invalid |
sca.amr |
always | ["hwk", "bio_strong", "face"] |
How the customer approved, from the PaSO key-binding JWT: hardware key, strong biometry, Face ID. | — |
sca.ts |
always | 2026-10-22T21:22:17Z |
When the customer approved. | — |
sca.credential |
always | urn:digitallabor:sparkasse:sca:1 |
With which credential: the Konto Karte. | — |
_sd |
always | ["7yDs…"] |
Digests of the disclosable claims below. | — |
_sd_alg |
always | sha-256 |
Digest algorithm. | — |
Selectively disclosable¶
| Field | Presence | Example | Meaning | Who asks for it |
|---|---|---|---|---|
agent_id |
always | agt_0db3f2b42011cdd0 |
The agent the mandate belongs to. | nobody; the merchant binds via cnf |
principal |
optional | cust_yuki_0001 |
The bank's customer reference — only if the customer disclosed customer_id of the Konto Karte during the approval; never taken from the mandate request. |
nobody; the agent presents the mandate credential to the merchant without disclosures |
Presentation¶
Only to the merchant, by the agent, together with the agent credential and the checkout part in the clear (§2.5): <issuer JWT>~<key-binding JWT> — no disclosures. Key-binding JWT signed with the agent key: nonce, aud = merchant id, iat, sd_hash (over exactly the presented <issuer JWT>~), transaction_data_hashes (the basket entry), transaction_data_hashes_alg; jti is recorded if present.
4. Checkout part (not a credential)¶
The product rules of the mandate. Built by the wallet, shown to the customer next to the payment part, signed by nobody: its hash went into the approved transaction_data (signed by the Konto Karte key) and into the mandate credential (signed by the bank). Held by wallet and agent, sent to the merchant in the clear at every purchase, kept in the merchant's log. The bank never sees it.
| Field | Presence | Example | Meaning |
|---|---|---|---|
checkout_mandate_id |
always | cm_5a7d… |
Id of this checkout part. |
mandate_request_id |
always | mrq_pending |
Placeholder: hash C is computed before the bank assigns the request id. |
functional_scope.category_scheme |
always | google_product_taxonomy:2021-09-21 |
Which category list the IDs below come from: the Google Product Taxonomy, version 2021-09-21. Missing in legacy checkout parts (mock words). |
functional_scope.allowed_categories |
always | ["422", "413", "630"] |
Google Product Taxonomy IDs the agent may buy from — here Food Items, Beverages, Household Supplies. A category includes all its subcategories. Empty = everything not excluded. |
functional_scope.excluded_categories |
always | ["499676", "435", "222"] |
IDs that are never allowed — here Alcoholic Beverages, Tobacco Products, Electronics. Exclusion wins over allowance. |
functional_scope.age_restricted_allowed |
always | false |
Whether age-restricted items may be bought. |
valid_from |
always | 2026-10-22T21:21:05Z |
Start, whole seconds. |
valid_until |
always | 2026-11-21T21:21:05Z |
End, whole seconds. |
Hash C = sha256: + hex SHA-256 over the canonical JSON of exactly these fields (sorted keys, no whitespace, / not escaped). Categories are Google Product Taxonomy IDs (Interface Specification §1.10.2: matching rule, legacy words). Legacy v2 checkout parts additionally carried principal_key, signature and a credential form urn:digitallabor:checkout_mandate:1 signed by the customer; the merchant still accepts them from app builds ≤ 792.
5. Status list token¶
How issuers revoke: one bit per credential, served as a signed token at /issuer/status/1 (bank and operator each have their own). Verifiers fetch it and check the bit at status.status_list.idx.
Header: alg ES256, typ statuslist+jwt, kid, x5c (bound to the host of the list).
| Field | Presence | Example | Meaning |
|---|---|---|---|
iss |
always | https://bank.s-you.me |
Issuer of the list. |
sub |
always | https://bank.s-you.me/issuer/status/1 |
URI of the list; must equal status.status_list.uri of the credential. |
iat |
always | 1792737549 |
Created at. |
ttl |
always | 300 |
Seconds a verifier may cache the list. |
status_list.bits |
always | 1 |
Bits per entry. |
status_list.lst |
always | eNqbxMjF… |
The bit string, zlib-compressed, base64url. Grows with the number of issued credentials. |
6. Proof JWT (issuance)¶
When a wallet or the agent redeems an offer, it proves possession of the key the credential will be bound to. Header: alg ES256, typ openid4vci-proof+jwt, jwk = the holder's public key (becomes cnf.jwk). Payload: iss (client id: wallet-dev for the wallet, eudipal-agent for the agent), aud = the issuer URL, iat, nonce from /issuer/nonce (single use). For the mandate credential the bank additionally requires jwk to be the mandate's agent key.
7. Payment confirmation (UCP token)¶
Not a credential: a compact JWS the bank returns with every executed payment (POST /payments → confirmation). The agent hands it to the merchant as payment.instruments[0].credential.token of the UCP payment handler de.digitallabor.bank_push; the merchant verifies it against the bank root without calling the bank (Interface Specification §2.6).
| Field | Presence | Example | Meaning |
|---|---|---|---|
header alg / typ / kid |
always | ES256 / payment-confirmation+jwt / issuer-1 |
same issuer key as the bank's credentials |
header x5c |
always | [issuer cert, root] |
chain to https://bank.s-you.me/issuer/root.pem |
iss |
always | https://bank.s-you.me |
the bank |
typ |
always | payment_confirmation |
— |
payment_id, mandate_id, merchant_id, amount, currency |
always | pay_…, mnd_…, merchant_kiezmarkt, 258, EUR |
the executed payment; the merchant compares merchant, mandate and amount with its checkout |
attestation_id, checkout_id |
always (null for non-UCP attestations) | att_…, chk_… |
binds the payment to the merchant's attestation and UCP checkout |
end_to_end_id, instrument, executed_at, status |
always | E2E…, sct_inst, 2026-10-25T08:05:34Z, executed |
settlement reference |
Keeping this page current¶
The tables between the markers <!-- fields:… --> and <!-- /fields --> are read by tools/mock-bank/credential_docs.py. The bank smokes call it with the credentials they actually obtain:
| Smoke | Checks |
|---|---|
smoke_issuer.py |
Konto Karte: header, payload, disclosures; status list payload |
smoke_agent.py |
agent credential: header, payload, disclosures |
smoke_mandate_vc.py |
mandate credential: header, payload, disclosures |
smoke_paso.py |
PaSO key-binding claims (/paso/.well-known) against the Konto Karte presentation table |
A smoke fails when an issued field is not documented here (a nested field is covered by a documented parent, e.g. cnf.jwk covers cnf.jwk.x), or when a field documented as always is missing in the issued credential. Changing a credential therefore means changing this page in the same commit.