PAAY 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. The payment provider needs to support EMV 3DS2 and accept the authentication data obtained from the card issuer.
In this proxy recipe, we'll detail how to initiate the authentication through PAAY without the card data ever passing through your own server.
The obtained authentication data can be sent to the payment provider in a similar fashion with another proxy call, but it is out of the scope of this recipe. Here we will just show how to call the /authenticate/browser endpoint.
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 authenticate with PAAY's 3DS APIuser: 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 this:
{
"request": {
"method": "POST",
"url": "https://api-sandbox.3dsintegrator.com/v2.2/authenticate/browser",
"headers": [
{
"Content-Type": "application/json"
},
{
"X-3DS-API-KEY": "<insert-api-key-here>"
},
{
"Authorization": "Bearer <insert-your-generated-JWT>"
}
],
"body": "{\"challengeIndicator\": \"01\", \"amount\": {{amount}}, \"currency\": \"{{ currency }}\", \"pan\": \"{{ card_number }}\", \"month\": \"{{ expiry_month }}\",\" year\": \"{{ expiry_year_short }}\", \"browser\": {\"browserAcceptHeader\": \"application/json\", \"browserJavaScriptEnabled\": {{ browser_javascript_enabled }}, \"browserJavaEnabled\": {{ browser_java_enabled }}, \"browserLanguage\": \"{{ browser_language }}\", \"browserColorDepth\": \"{{ browser_color_depth }}\", \"browserTZ\": \"{{ browser_tz }}\", \"browserScreenWidth\": \"{{ browser_screen_width }}\", \"browserScreenHeight\": \"{{ browser_screen_height }}\", \"browserUserAgent\": \"{{ browser_user_agent }}\"}, \"threeDSRequestorURL\": \"{{ requestor_url }}\"}",
"extra_data": {
"amount": 100,
"currency": "USD",
"browser_javascript_enabled": true,
"browser_java_enabled": false,
"browser_language": "en-US", // browser capabilities obtained with javascript on the client side e.g. window.navigator.language
"browser_color_depth": 32,
"browser_tz": 120,
"browser_screen_width": 1920,
"browser_screen_height": 1080,
"browser_user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36",
"requestor_url": "https://my-online-shop.com"
}
},
"synchronous": true
}
We define the proxy request as being a "POST" method, having content-type header of application/json and we also set our PAAY JWT and API key as a headers. For the body we'll set a mustache template, where PCI Vault will populate the values from the vault before sending the request. 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.
One optional parameter that is set in this example is "synchronous": true. This will tell PCI Vault to wait for the response from PAAY to be returned and will send the result back to us when the request completes.
For the body field in the request we set a JSON object with placeholders for all the fields where data will be interpolated by PCI Vault. All the card data fields used are as they are stored in the vault (yours might vary), whereas any additional fields which aren't sensitive we'll pass in the extra_data object.
This request body will tell PAAY about the card details, amount to be charged and the capabilities of the user's browser. Note: the body template needs to be stringified JSON.
{
"challengeIndicator": "01",
"amount": {{amount}},
"currency": "{{ currency }}",
"pan": "{{ card_number }}",
"month": "{{ expiry_month }}",
"year": "{{ expiry_year_short }}",
"browser": {
"browserAcceptHeader": "application/json",
"browserJavaScriptEnabled": {{ browser_javascript_enabled }},
"browserJavaEnabled": {{ browser_java_enabled }},
"browserLanguage": "{{ browser_language }}",
"browserColorDepth": "{{ browser_color_depth }}",
"browserTZ": "{{ browser_tz }}",
"browserScreenWidth": "{{ browser_screen_width }}",
"browserScreenHeight": "{{ browser_screen_height }}",
"browserUserAgent": "{{ browser_user_agent }}"
},
"threeDSRequestorURL": "{{ requestor_url }}"
}
The response from PAAY 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://api-sandbox.3dsintegrator.com/v2.2/authenticate/browser",
"headers": [
{
"Content-Type": "application/json"
},
{
"X-3DS-API-KEY": "<insert-api-key-here>"
},
{
"Authorization": "Bearer <insert-your-generated-JWT>"
}
],
"body": "...",
"extra_data": {
"amount": 100,
"currency": "USD",
"browser_tz": 120,
"browser_screen_width": 1920,
"browser_screen_height": 1080,
"browser_user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36",
"requestor_url": "https://my-online-shop.com"
}
},
"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; charset=utf-8"
},
{
"X-3DS-CORRELATION-ID": "a2fd247a-f4df-48b7-aca1-54f44272dd4a"
},
{
"X-3DS-TRANSACTION-ID": "081b305f-de1a-43ce-a2db-63bdc3d2c2e3"
}
],
"body": "{\"methodURL\": \"https://acs-server.3dsintegrator.com/v2.2/fingerprint\",\"protocolVersion\": \"2.2.0\",\"correlationId\": \"a2fd247a-f4df-48b7-aca1-54f44272dd4a\",\"transactionId\": \"081b305f-de1a-43ce-a2db-63bdc3d2c2e3\",\"threeDSMethodData\": \"eyJ0aHJlZURTTWV0aG9kTm90aWZpY2F0aW9uVVJMIjoiaHR0cHM6Ly9yZXNwb25zZS1zYW5kYm94LmRldi4zZHNpbnRlZ3JhdG9yLmNvbS9maW5nZXJwcmludCIsInRocmVlRFNTZXJ2ZXJUcmFuc0lEIjoiMDgxYjMwNWYtZGUxYS00M2NlLWEyZGItNjNiZGMzZDJjMmUzIn0=\",\"scaIndicator\": false}",
"status_code": 200
}
}
The response from PAAY is in the result field. The body is returned as a JSON string, which must be parsed. The correlation ID and transaction ID values needed for the following steps can either be obtained from the body or from the headers, the PAAY API returns them both.
Now that we have initiated the card authentication with PAAY, we use the response to render an iframe which POSTs the unique threeDSMethodData to the methodURL. This will do the device fingerprinting. You can learn more about the rest of the process by reading the PAAY guide.