Adyen allows merchants to authenticate cards with EMV 3DS (Europay Mastercard Visa 3 Domain Secure), before forwarding the payment data to the payment provider. This shifts the liability to the card issuer in case of fraud or disputes. After obtaining the authentication data from the card issuer, you have the option to authorize the payment through Adyen or any other payment provider which supports EMV 3DS2 and accepts this data.
In this proxy recipe, we'll detail how to cater to the use-case, where card data is forwarded to Adyen to authenticate the card holder with 3D Secure without the sensitive card data ever passing through your own server.
In this proxy recipe, we'll show you how to:
We'll use the Proxy Post endpoint to initiate a proxy request. The following query string parameters are required:
token: the token identifying the card we'd like to send to Adyenuser: the key used to encrypt the datapassphrase: the passphrase for the key used to encrypt the dataThe request URL will look like https://api.pcivault.io/v1/proxy/post?token=uniquetoken&user=ABCD&passphrase=secretpassphrase
The request body will look something like:
{
"request": {
"method": "POST",
"url": "https://yoursubdomain.adyen.com/v71/payments", //remember to set your account-specific subdomain here
"headers": [
{
"Content-Type": "application/json"
},
{
"X-API-Key": "{{api_key}}"
},
{
"Idempotency-Key": "{{idempotency_key}}"
}
],
"body": "{\"amount\": {\"currency\": \"{{currency}}\", \"value\": {{amount}}}, \"reference\": \"{{reference}}\", \"paymentMethod\": {\"type\": \"scheme\", \"number\": \"{{card_number}}\", \"expiryMonth\": \"{{expiry_month}}\", \"expiryYear\": \"{{expiry_year}}\", \"holderName\": \"{{card_holder}}\", \"cvc\": \"{{cvv}}\"}, \"channel\": \"web\", \"authenticationData\": {\"authenticationOnly\": true}, \"browserInfo\": {\"userAgent\": \"{{ browser_user_agent }}\", \"acceptHeader\": \"{{ browser_accept_header }}\", \"language\": \"{{ browser_language }}\", \"colorDepth\": {{ browser_color_depth }}, \"screenWidth\": {{ browser_screen_width }}, \"screenHeight\": {{ browser_screen_height }}, \"timeZoneOffset\": {{ browser_tz }}, \"javaEnabled\": {{ browser_java_enabled }}, \"shopperIP\": \"{{ shopper_ip }}\", \"shopperEmail\": \"{{ shopper_email }}\", \"origin\": \"https://your-company.example.com\", \"returnUrl\": \"{{return_url}}\", \"merchantAccount\": \"{{merchant_account}}\"}",
"extra_data": {
"api_key": "your-adyen-api-key",
"idempotency_key": "uuid-generated-for-request",
"currency": "USD",
"amount": 1000,
"reference": "youruniqueref",
"return_url": "https://your-company.example.com/...",
"merchant_account": "yourmerchantaccount",
"browser_user_agent": "Mozilla\/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/70.0.3538.110 Safari\/537.36",
"browser_accept_header": "text\/html,application\/xhtml+xml,application\/xml;q=0.9,image\/webp,image\/apng,*\/\*;q=0.8",
"browser_language": "nl-NL", // browser capabilities obtained with javascript on the client side e.g. window.navigator.language
"browser_color_depth": 24,
"browser_screen_width": 1280,
"browser_screen_height": 768,
"browser_tz": 0,
"browser_java_enabled": false,
"shopper_ip": "127.0.0.1"
}
},
"synchronous": true
}
(Note: this request uses field names as they would be captured by our Hosted Form, if your data is in a different format, these field names have to be adjusted accordingly)
We define the proxy request as being a "POST" method, having content-type header of application/json and we also set our Adyen API key and unique idempotency key as headers. For the body we'll set a mustache template, where PCI Vault will populate the values from the vault before sending the request to Adyen. Note that one can construct the body template string with fields already populated or like in this case, set an extra_data object whose fields will be merged with the stored data before being populated in the mustache template. Using extra_data is often simpler and cleaner than string concatenation.
The parsed JSON for the body field looks like:
{
"amount": {
"currency": "{{currency}}",
"value": {{amount}}
},
"reference": "{{reference}}",
"paymentMethod": {
"type": "scheme",
"number": "{{card_number}}",
"expiryMonth": "{{expiry_month}}",
"expiryYear": "{{expiry_year}}",
"holderName": "{{card_holder}}",
"cvc": "{{cvv}}"
},
"channel": "web",
"authenticationData": {
"authenticationOnly": true // this triggers the standalone authentication
},
"browserInfo": {
"userAgent": "{{ browser_user_agent }}",
"acceptHeader": "{{ browser_accept_header }}",
"language": "{{ browser_language }}",
"colorDepth": {{ browser_color_depth }},
"screenWidth": {{ browser_screen_width }},
"screenHeight": {{ browser_screen_height }},
"timeZoneOffset": {{ browser_tz }},
"javaEnabled": {{ browser_java_enabled }
},
"shopperIP": "{{ shopper_ip }}",
"shopperEmail": "{{ shopper_email }}",
"origin": "https://your-company.example.com",
"returnUrl": "{{return_url}}",
"merchantAccount": "{{merchant_account}}"
}
This request body will tell Adyen about the card details, amount to be charged and the capabilities of the user's browser.
One optional parameter that is set in this example is "synchronous": true. This will tell PCI Vault to wait for the Adyen response to be returned and will send the result back to us when the request completes.
The response from Adyen will be wrapped in a JSON object like this:
{
"id": "SLHpcudvjdUCokB2UCkR4n",
"status": "success",
"type": "post",
"use_static_ip": false,
"synchronous": true,
"request": {
"method": "POST",
"url": "https://yoursubdomain.adyen.com/v71/payments",
"headers": [
{
"content-type": "application/json"
},
{
"X-API-Key": "{{api_key}}"
},
{
"Idempotency-Key": "{{idempotency_key}}"
}
],
"body": "...",
"extra_data": {
"api_key": "your-adyen-api-key",
"idempotency_key": "uuid-generated-for-request",
"currency": "USD",
"amount": 1000,
"reference": "youruniqueref",
"return_url": "https://your-company.example.com/...",
"merchant_account": "yourmerchantaccount",
"browser_user_agent": "Mozilla\/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/70.0.3538.110 Safari\/537.36",
"browser_accept_header": "text\/html,application\/xhtml+xml,application\/xml;q=0.9,image\/webp,image\/apng,*\/\*;q=0.8",
"browser_language": "nl-NL",
"browser_color_depth": 24,
"browser_screen_width": 1280,
"browser_screen_height": 768,
"browser_tz": 0,
"browser_java_enabled": false,
"shopper_ip": "127.0.0.1"
}
},
"webhook": {
"status": "pending"
},
"max_attempts": 1,
"attempts": 0,
"debug_mode": false,
"received": "2025-11-12T14:06:06.621177+00:00",
"result": {
"headers": [
{
"Content-Type": "application/json"
},
...
],
"body": "{\"resultCode\": \"ChallengeShopper\",\"merchantReference\": \"youruniqueref\"...}",
"status_code": 200
}
}
The response is in the result field. The body is returned as a JSON string, which can be parsed and handled as needed.
We need to look at the resultCode field in the result body to make a decision on what to do next. If the resultCode is any of the following:
ChallengeShopper: The issuer requires authentication from the shopper, for example through facial recognition or entering a code received on their phone. Initiate the challenge flow to present the challenge to the shopper, and submit the result to Adyen.IdentifyShopper: The issuer requires authentication from the device the shopper is using. Initiate the frictionless flow to get the shopper's device fingerprint, and submit the result to Adyen.RedirectShopper: The issuer requires the shopper to provide authentication in an external web page or app. Redirect the shopper to complete the authentication.More information can be found regarding the different flows in the Adyen documentation.
After obtaining the authentication result, it needs to be sent through to the payments/details endpoint in the format:
{
"details": {
"threeds2.fingerprint": "eyJ0cmFuc1N0YXR1cyI6IlkifQ=="
},
"authenticationData": {
"authenticationOnly": true
}
}
This request will return the 3DS2 result needed when authorizing the payment.
{
"pspReference": "V4HZ4RBFJGXXGN82",
"resultCode": "AuthenticationFinished",
"merchantReference": "youruniqueref",
"threeDS2Result": {
"authenticationValue": "QURZRU4gM0RTMiBURVNUIENBVlY=",
"dsTransID": "a3b86754-444d-46ca-95a2-ada351d3f42c",
"eci": "05",
"messageVersion": "2.2.0",
"threeDSServerTransID": "6edcc246-23ee-4e94-ac5d-8ae620bea7d9",
"transStatus": "Y",
"challengeCancel": "00",
"threeDSRequestorChallengeInd": "01",
"exemptionIndicator": "lowValue",
"riskScore": "95",
"transStatusReason": "02",
"cavvAlgorithm": "ABC"
},
"threeDSPaymentData": "Ab02b4c0!BQABAgA...==",
}