3D Secure
Quick Reference
How 3DS works with A55
Set threeds_authentication: true on the charge and supply a rich device_info object. A55 handles the 3DS protocol, routes to the issuer, and returns the result.
The JavaScript SDK runs Device Data Collection (DDC) automatically via A55Pay.authentication(). Server-side integrations (H2H) send device_info fields in the charge JSON.
Frictionless vs challenge
| Aspect | Frictionless | Challenge |
|---|---|---|
| Decision | Issuer approves silently based on risk score | Issuer requires cardholder interaction |
| UX impact | Zero (invisible to buyer) | Redirect to url_3ds or iframe popup |
| When it happens | Low-risk + rich device data | High-risk or issuer policy |
| Conversion | Highest | Lower (adds friction) |
| Liability shift | Yes | Yes |
| Typical rate | 70–85% of 3DS transactions | 15–30% |
ECI values
Step-by-step integration
Collect device_info from the browser
Capture real browser values on the client side. Every field improves the issuer's risk model.
- JavaScript
- Python (backend)
const deviceInfo = {
ip_address: await fetch('/api/ip').then(r => r.text()),
user_agent: navigator.userAgent,
http_accept_content: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
http_accept_browser_value: '*/*',
http_browser_language: navigator.language,
http_browser_java_enabled: navigator.javaEnabled?.() ?? false,
http_browser_javascript_enabled: true,
http_browser_color_depth: String(screen.colorDepth),
http_browser_screen_height: String(screen.height),
http_browser_screen_width: String(screen.width),
http_browser_time_difference: String(new Date().getTimezoneOffset()),
};
// Send deviceInfo to your backend for the charge request
# Receive device_info from your frontend
device_info = request.json.get("device_info", {})
# Validate required fields
required = [
"user_agent", "ip_address", "http_accept_content",
"http_browser_language", "http_browser_screen_height",
"http_browser_screen_width", "http_browser_time_difference",
"http_accept_browser_value", "http_browser_color_depth",
"http_browser_java_enabled", "http_browser_javascript_enabled",
]
missing = [f for f in required if f not in device_info]
if missing:
raise ValueError(f"Missing device_info fields: {missing}")
Create a charge with threeds_authentication: true
- cURL
- Python
- Node.js
- Go
- Java
- PHP
curl -sS -X POST "https://sandbox.api.a55.tech/api/v1/bank/wallet/charge/" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"wallet_uuid": "00000000-0000-4000-8000-000000000001",
"type_charge": "credit_card",
"card_name": "JANE DOE",
"card_number": "4111111111111111",
"card_expiry_month": 12,
"card_expiry_year": 2030,
"card_cvv": "123",
"amount": 10000,
"currency": "BRL",
"installment_count": 1,
"threeds_authentication": true,
"device_info": {
"ip_address": "203.0.113.10",
"user_agent": "Mozilla/5.0 ...",
"http_accept_content": "text/html",
"http_accept_browser_value": "*/*",
"http_browser_language": "pt-BR",
"http_browser_java_enabled": false,
"http_browser_javascript_enabled": true,
"http_browser_color_depth": "24",
"http_browser_screen_height": "900",
"http_browser_screen_width": "1440",
"http_browser_time_difference": "180"
},
"webhook_url": "https://merchant.example/webhooks/a55",
"redirect_url": "https://merchant.example/return"
}'
import os, requests
r = requests.post(
"https://sandbox.api.a55.tech/api/v1/bank/wallet/charge/",
headers={"Authorization": f"Bearer {os.environ['ACCESS_TOKEN']}",
"Content-Type": "application/json"},
json={
"wallet_uuid": "00000000-0000-4000-8000-000000000001",
"type_charge": "credit_card",
"card_name": "JANE DOE",
"card_number": "4111111111111111",
"card_expiry_month": 12,
"card_expiry_year": 2030,
"card_cvv": "123",
"amount": 10000,
"currency": "BRL",
"threeds_authentication": True,
"device_info": {
"ip_address": "203.0.113.10",
"user_agent": "Mozilla/5.0 ...",
"http_accept_content": "text/html",
"http_accept_browser_value": "*/*",
"http_browser_language": "pt-BR",
"http_browser_java_enabled": False,
"http_browser_javascript_enabled": True,
"http_browser_color_depth": "24",
"http_browser_screen_height": "900",
"http_browser_screen_width": "1440",
"http_browser_time_difference": "180",
},
"webhook_url": "https://merchant.example/webhooks/a55",
"redirect_url": "https://merchant.example/return",
},
)
print(r.status_code, r.json())
const res = await fetch(
"https://sandbox.api.a55.tech/api/v1/bank/wallet/charge/",
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ACCESS_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
wallet_uuid: "00000000-0000-4000-8000-000000000001",
type_charge: "credit_card",
card_name: "JANE DOE",
card_number: "4111111111111111",
card_expiry_month: 12,
card_expiry_year: 2030,
card_cvv: "123",
amount: 10000,
currency: "BRL",
threeds_authentication: true,
device_info: {
ip_address: "203.0.113.10",
user_agent: "Mozilla/5.0 ...",
http_accept_content: "text/html",
http_accept_browser_value: "*/*",
http_browser_language: "pt-BR",
http_browser_java_enabled: false,
http_browser_javascript_enabled: true,
http_browser_color_depth: "24",
http_browser_screen_height: "900",
http_browser_screen_width: "1440",
http_browser_time_difference: "180",
},
webhook_url: "https://merchant.example/webhooks/a55",
redirect_url: "https://merchant.example/return",
}),
}
);
console.log(await res.json());
payload := map[string]interface{}{
"wallet_uuid": "00000000-0000-4000-8000-000000000001",
"type_charge": "credit_card",
"card_name": "JANE DOE", "card_number": "4111111111111111",
"card_expiry_month": 12, "card_expiry_year": 2030, "card_cvv": "123",
"amount": 10000, "currency": "BRL",
"threeds_authentication": true,
"device_info": map[string]interface{}{
"ip_address": "203.0.113.10", "user_agent": "Mozilla/5.0 ...",
"http_accept_content": "text/html", "http_accept_browser_value": "*/*",
"http_browser_language": "pt-BR", "http_browser_java_enabled": false,
"http_browser_javascript_enabled": true, "http_browser_color_depth": "24",
"http_browser_screen_height": "900", "http_browser_screen_width": "1440",
"http_browser_time_difference": "180",
},
"webhook_url": "https://merchant.example/webhooks/a55",
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST",
"https://sandbox.api.a55.tech/api/v1/bank/wallet/charge/", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+os.Getenv("ACCESS_TOKEN"))
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
io.Copy(os.Stdout, resp.Body)
String json = """
{"wallet_uuid":"00000000-0000-4000-8000-000000000001",
"type_charge":"credit_card","card_name":"JANE DOE",
"card_number":"4111111111111111","card_expiry_month":12,
"card_expiry_year":2030,"card_cvv":"123","amount":10000,
"currency":"BRL","threeds_authentication":true,
"device_info":{"ip_address":"203.0.113.10","user_agent":"Mozilla/5.0",
"http_accept_content":"text/html","http_accept_browser_value":"*/*",
"http_browser_language":"pt-BR","http_browser_java_enabled":false,
"http_browser_javascript_enabled":true,"http_browser_color_depth":"24",
"http_browser_screen_height":"900","http_browser_screen_width":"1440",
"http_browser_time_difference":"180"},
"webhook_url":"https://merchant.example/webhooks/a55"}
""";
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://sandbox.api.a55.tech/api/v1/bank/wallet/charge/"))
.header("Authorization", "Bearer " + System.getenv("ACCESS_TOKEN"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(json)).build();
HttpResponse<String> resp = HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(resp.body());
$payload = [
"wallet_uuid" => "00000000-0000-4000-8000-000000000001",
"type_charge" => "credit_card",
"card_name" => "JANE DOE", "card_number" => "4111111111111111",
"card_expiry_month" => 12, "card_expiry_year" => 2030, "card_cvv" => "123",
"amount" => 10000, "currency" => "BRL",
"threeds_authentication" => true,
"device_info" => [
"ip_address" => "203.0.113.10", "user_agent" => "Mozilla/5.0",
"http_accept_content" => "text/html", "http_accept_browser_value" => "*/*",
"http_browser_language" => "pt-BR", "http_browser_java_enabled" => false,
"http_browser_javascript_enabled" => true, "http_browser_color_depth" => "24",
"http_browser_screen_height" => "900", "http_browser_screen_width" => "1440",
"http_browser_time_difference" => "180",
],
"webhook_url" => "https://merchant.example/webhooks/a55",
];
$ch = curl_init("https://sandbox.api.a55.tech/api/v1/bank/wallet/charge/");
curl_setopt_array($ch, [
CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("ACCESS_TOKEN"),
"Content-Type: application/json"],
CURLOPT_POSTFIELDS => json_encode($payload),
]);
echo curl_exec($ch);
Handle the response
The response always returns the full charge object. The key fields that change by outcome are status, action_url, and message.
- Frictionless (confirmed)
- Challenge (pending)
- Authentication failed
{
"charge_uuid": "a34f2499-2a40-40a5-8f5a-578002a88f51",
"local_currency": 159.8,
"currency": "EUR",
"usd_currency": 185.59,
"type": "credit_card",
"date": "2026-04-01",
"description": "#18770 Payments Cabo HDMI 2.1 Ultra 8K",
"due_date": "2025-10-11",
"status": "confirmed",
"message": [],
"installment_count": 1,
"installments": [],
"pix_payload": {},
"qra_payload": {},
"applepay_payload": {},
"charge_payment_url": null,
"action_url": "",
"session_id": null,
"subscription": {},
"reference_external_id": "10ccf81a-1de7-4a3e-b86d-f3f685e8ee57",
"is_async": false
}
No redirect needed — the charge was authenticated silently.
When a 3DS challenge or fingerprint collection is required, the response includes action_url and status: "pending". Redirect the payer to action_url to complete the flow. After completion, the payer is redirected to your redirect_url and a webhook is sent to webhook_url.
{
"charge_uuid": "fe5e2c1e-b3fb-4cc4-bb28-004da37d5804",
"local_currency": 159.8,
"currency": "EUR",
"usd_currency": 185.52,
"type": "credit_card",
"date": "2026-04-01",
"description": "#18770 Payments Cabo HDMI 2.1 Ultra 8K",
"due_date": "2025-10-11",
"status": "pending",
"message": null,
"installment_count": 1,
"installments": [],
"pix_payload": {},
"qra_payload": {},
"applepay_payload": {},
"charge_payment_url": null,
"action_url": "https://acquirer.example/payment/69cd4a32e367...",
"session_id": null,
"subscription": {},
"reference_external_id": "e4e3ec12-b51a-4ea2-aceb-2b41602a5a84",
"is_async": false
}
Redirect the buyer to action_url and proceed to Step 4.
{
"charge_uuid": "a34f2499-2a40-40a5-8f5a-578002a88f51",
"local_currency": 159.8,
"currency": "EUR",
"usd_currency": 185.59,
"type": "credit_card",
"date": "2026-04-01",
"description": "#18770 Payments Cabo HDMI 2.1 Ultra 8K",
"due_date": "2025-10-11",
"status": "error",
"message": [
{
"code": "charge_error_issued",
"description": "Missing required fields: remote_ip"
}
],
"installment_count": 1,
"installments": [],
"pix_payload": {},
"qra_payload": {},
"applepay_payload": {},
"charge_payment_url": null,
"action_url": "",
"session_id": null,
"subscription": {},
"reference_external_id": "10ccf81a-1de7-4a3e-b86d-f3f685e8ee57",
"is_async": false
}
Handle the challenge redirect (if pending)
If the response includes action_url, redirect the buyer to that URL. After the payer completes the 3DS challenge or fingerprint collection, they are automatically redirected to your redirect_url. Always rely on the webhook for the final transaction status.
if (response.action_url) {
window.location.href = response.action_url;
}
- Your page redirects the payer to
action_url - The payer completes the required action (3DS OTP, biometrics, fingerprint, etc.)
- After completion, the payer is redirected to your
redirect_url - A55 sends a webhook to
webhook_urlwith the final transaction status (paidorerror)
Confirm via webhook
Always use the webhook as the source of truth. Configure webhook_url on the charge so you receive async updates even if the buyer closes the browser.
{
"charge_uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "paid",
"transaction_reference": "txn_ref_001"
}
End-to-end flow
device_info fields
| Field | Type | Required | Description |
|---|---|---|---|
ip_address | string | Yes | Client public IP |
user_agent | string | Yes | Browser user agent string |
http_accept_content | string | Yes | HTTP Accept header |
http_accept_browser_value | string | Yes | Browser Accept value |
http_browser_language | string | Yes | Browser language (e.g., pt-BR) |
http_browser_java_enabled | boolean | Yes | Java plugin enabled |
http_browser_javascript_enabled | boolean | Yes | JavaScript enabled |
http_browser_color_depth | string | Yes | Screen color depth |
http_browser_screen_height | string | Yes | Screen height in pixels |
http_browser_screen_width | string | Yes | Screen width in pixels |
http_browser_time_difference | string | Yes | UTC offset in minutes |
device_id | string | No | Stable device identifier |
session_id | string | No | Checkout session ID |
Incomplete or synthetic device_info increases challenge rates and declines. Collect real browser values on the client and pass them to your server. Never hardcode these values.
SDK DDC (A55Pay.authentication)
When using the JavaScript SDK, you can run DDC standalone:
A55Pay.authentication({
transactionReference: 'charge-uuid-or-ref',
cardBrand: 'visa',
cardNumber: '4111111111111111',
cardExpiryMonth: '12',
cardExpiryYear: '2030',
onSuccess: (payload) => {
// payload.sessionId, payload.accessToken, payload.referenceId
// Use sessionId for the charge request
},
onError: (err) => {
// DDC failed — proceed without session or handle error
},
});
Note: payV2() runs DDC automatically before processing. You only need authentication() if you are managing the flow manually.
Validation endpoint
POST /api/v1/bank/public/charge/authentication/{uuid}/validate
Use the charge uuid from the 3DS flow. A55 validates the authentication metadata and returns the final result.
Test with sandbox cards
| Card Number | Brand | Scenario | Expected Status |
|---|---|---|---|
4111 1111 1111 1111 | Visa | 3DS frictionless — confirmed | confirmed |
4000 0000 0000 0101 | Visa | 3DS challenge required | confirmed |
4000 0000 0000 0002 | Visa | 3DS — declined by issuer | declined |
5500 0000 0000 0004 | Mastercard | 3DS frictionless — confirmed | confirmed |
4000 0000 0000 0069 | Visa | 3DS — processing error | error |
Want higher approval rates without challenges?
Visa Data Only shares enriched transaction data with issuers through 3DS rails — without a single challenge redirect. Merchants in Square's pilot saw up to +646 basis points improvement over 9 months across 6 million transactions.
No friction. No cart abandonment. A55 supports this today with one flag: data_only: true.
The trade-off: no liability shift (merchant bears fraud). For high-volume, low-risk transactions in LATAM, this trade-off often pays for itself many times over.