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://core-manager.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://core-manager.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://core-manager.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://core-manager.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://core-manager.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://core-manager.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
- Frictionless (confirmed)
- Challenge (pending)
- Authentication failed
{
"uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "confirmed",
"eci": "05",
"authentication_status": "success",
"reference_external_id": "ord_001"
}
No redirect needed — the charge was authenticated silently.
{
"uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "pending",
"url_3ds": "https://confirmacion.a55.tech/charge/a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"reference_external_id": "ord_001"
}
Redirect the buyer to url_3ds and proceed to Step 4.
{
"uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "error",
"message": [{"message": "3DS authentication failed"}],
"code": "error_threeds_authentication"
}
Handle the 3DS challenge (if pending)
If the response includes url_3ds, redirect the buyer. After the challenge, listen for postMessage:
window.addEventListener('message', function (event) {
if (event.data?.event === '3ds-auth-complete') {
const chargeUuid = event.data.chargeUuid;
// Fetch charge status from your backend, then update UI
}
});
- Your page redirects (or iframes) to
confirmacion.a55.tech/charge/{uuid} - The 3DS page loads the issuer's ACS (Access Control Server) in a nested iframe
- The cardholder completes verification (OTP, biometrics, etc.)
- The ACS redirects to
confirmacion.a55.tech/validate/{uuid} - A55 validates the authentication and sends
postMessagewith3ds-auth-completeback up the window chain
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.