Skip to main content

Use transactional data

Transactional data (the transaction_data parameter in OID4VP) is an optional parameter that binds contextual information to a presentation request. The wallet displays this data to the user during the consent step and authenticates it by including it in the device binding signature. The hashes returned in the response are a product of this cryptographic binding, allowing you to verify that the user approved the specific context. This guide shows you how to construct, encode, and verify transactional data with the Truvity EUDIW Connector.

Transactional data is a general mechanism applicable to any use case: session binding, document references, payment authorization, or any scenario where you need to prove the user consented to a specific context.

note

Transactional data is the mechanism through which EUDI Wallets support Strong Customer Authentication (SCA)—a requirement under the Payment Services Directive (PSD2), the EU regulation governing electronic payment services, that mandates two of three authentication factors (knowledge, possession, inherence) for electronic payments. In the SCA context, the transactional data must include at least the payment amount and the payee. The SCA use case requires a prior registration step where the user's wallet receives a dedicated Secure User Authentication (SUA) attestation—a credential issued to the wallet during a registration step with the Relying Party that authorizes the wallet to perform SCA-protected operations.

Prerequisites
  • 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/payment). 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
  • Familiarity with creating presentation requests (see getting started)

Overview

You build a presentation request that includes transactional data, then verify in the callback that the wallet acknowledged the bound data. This ensures that a presentation intended for one transaction cannot be replayed in a different context.

note

The structure and semantics of transactional data objects depend on the applicable Attestation Rulebook for the credential type being requested. The examples in this guide are illustrative; production implementations should follow the Rulebook for their specific use case. The ARF also defines transactional data support for proximity flows via ISO/IEC 18013-5; this guide covers the remote (OID4VP) flow implemented by the connector.

Time to implement: 1 hour.

Step 1: Construct transactional data objects

Each transactional data object contains a type field that identifies the transaction context and a credential_ids array referencing DCQL credential IDs that can authorize the transaction. These two fields are defined by the OID4VP specification. Additional fields (for example, amount and payee for a payment context) are defined by the applicable Attestation Rulebook for the credential type being requested.

Examples for common use cases:

Session binding—bind a login session to the presentation:

{
"type": "login_session",
"credential_ids": ["pid_auth"]
}

Document reference—bind a document signing context to the presentation:

{
"type": "document_signing",
"credential_ids": ["pid_kyc"]
}

Payment context—bind a payment authorization to the presentation:

{
"type": "payment_authorization",
"credential_ids": ["pid_kyc"]
}

Step 2: Include transactional data in the request

Include the transactional data objects in the transaction_data array of the POST /oidc4vp request body. The connector serializes and base64url-encodes the objects before including them in the authorization request JWT's transaction_data field.

curl -X POST http://connector:8081/oidc4vp \
-H "Content-Type: application/json" \
-d '{
"dcql_query": {
"credentials": [
{
"id": "pid_kyc",
"format": "dc+sd-jwt",
"meta": { "vct_values": ["urn:eudi:pid:1"] },
"claims": [
{ "path": ["given_name"] },
{ "path": ["family_name"] }
]
}
]
},
"transaction_data": [
{
"type": "payment_authorization",
"credential_ids": ["pid_kyc"]
}
]
}'

Example response:

{
"state": "abc123",
"same_device_request_uri": "openid4vp://?client_id=...&request_uri=https%3A%2F%2Fconnector.example.com%2Foidc4vp%2Fabc123%2Frequest%3Fflow_type%3Dsame-device",
"cross_device_request_uri": "openid4vp://?client_id=...&request_uri=https%3A%2F%2Fconnector.example.com%2Foidc4vp%2Fabc123%2Frequest%3Fflow_type%3Dcross-device"
}

Store the state value for verification in the callback.

Step 3: Verify transactional data in the callback

When the callback arrives with a FULFILLED status, each PresentedCredential includes a transactionDataHashes array. These hashes are a product of the wallet's device binding signature—the wallet authenticates the transactional data by including it in the device binding process, so no separate signed response field is needed. Verify these hashes against locally computed hashes of the original base64url-encoded transactional data strings to confirm the wallet acknowledged the bound data.

# Simulate a FULFILLED callback with transactionDataHashes for testing.
# In production, the connector delivers this payload to your callback endpoint.
curl -X POST http://localhost:3000/callback \
-H "Content-Type: application/json" \
-d '{
"status": "FULFILLED",
"state": "abc123",
"responseCode": "<correlation-token>",
"credentials": {
"pid_kyc": [
{
"issuer": "https://issuer.example.com",
"claims": {
"given_name": "Erika",
"family_name": "Mustermann"
},
"signatureIsValid": true,
"supportRevocation": false,
"supportTrustAnchor": true,
"isTrusted": false,
"kbKeyId": "<key-id-per-rfc7638>",
"kbSignatureIsValid": true,
"transactionDataHashes": ["<base64url-encoded-sha256-hash>"],
"validFrom": "2026-01-15T10:30:00Z",
"validUntil": "2026-07-15T10:30:00Z"
}
]
},
"credentialsRaw": {
"pid_kyc": [
{
"claims": "<base64-encoded-claims-json>",
"issuer": "https://issuer.example.com",
"kbKeyId": "<key-id-per-rfc7638>",
"transactionDataHashes": ["<base64url-encoded-sha256-hash>"],
"validFrom": "2026-01-15T10:30:00Z",
"validUntil": "2026-07-15T10:30:00Z"
}
]
}
}'

The responseCode field is present only in same-device flows. If you use a cross-device flow, the callback omits responseCode.

Testing

Test checklist

  • Transactional data encodes correctly as base64url strings
  • Presentation request includes transaction_data in the request body
  • Callback contains transactionDataHashes in the PresentedCredential
  • Hash verification succeeds for matching transactional data
  • Hash verification fails for tampered transactional data
  • Multiple transactional data items encode and verify correctly

Troubleshooting

Missing transactionDataHashes in callback

Verify that you included the transaction_data array in the POST /oidc4vp request body. The transactionDataHashes field is only present when transactional data was included in the original request.

Hash mismatch

Ensure you compute the hash over the exact base64url-encoded string you sent in the request, not the decoded JSON. The hash input is the encoded string, not the raw JSON object.

Next steps

  • KYC verification—implement identity verification with transactional data binding
  • Error handling—handle all verification outcomes in your callback

Further reading

  • DCQL query language—how to specify credential requirements and transactional data context
  • OID4VP protocol—how transactional data is bound to the authorization request
  • Device binding—how the wallet cryptographically signs transactional data
  • Error codes—wallet-facing HTTP error responses
  • Callback events—Presented Credentials Event statuses and payload fields