3D Secure 验证
Quick Reference
3DS 在 A55 中的工作方式
在收费上设置 threeds_authentication: true,并提供完整的 device_info 对象。A55 处理 3DS 协议、路由至发卡行并返回结果。
JavaScript SDK 通过 A55Pay.authentication() 自动运行设备数据采集(DDC,Device Data Collection)。服务端集成 H2H(主机到主机直连)在收费 JSON 中发送 device_info 字段。
无摩擦(frictionless)与 challenge
| 方面 | 无摩擦(Frictionless) | Challenge |
|---|---|---|
| 决策 | 发卡行根据风险分静默批准 | 发卡行要求持卡人交互 |
| 对体验的影响 | 零(买家无感) | 跳转至 url_3ds 或 iframe 弹窗 |
| 何时发生 | 低风险且设备数据丰富 | 高风险或发卡行策略 |
| 转化 | 最高 | 较低(增加摩擦) |
| 责任转移 | 是 | 是 |
| 典型占比 | 3DS 交易的 70–85% | 15–30% |
ECI 值
分步集成
从浏览器采集 device_info
在客户端采集真实浏览器值。每个字段都有助于发卡行的风险模型。
- 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()),
};
// 将 deviceInfo 发送至后端用于收费请求
# 从前端接收 device_info
device_info = request.json.get("device_info", {})
# 验证必填字段
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"缺少 device_info 字段:{missing}")
创建收费并设置 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": "张伟",
"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": "张伟",
"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: "张伟",
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": "张伟", "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":"张伟",
"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" => "张伟", "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);
处理响应
- 无摩擦(已通过)
- Challenge(待处理)
- 认证失败
{
"uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "confirmed",
"eci": "05",
"authentication_status": "success",
"reference_external_id": "ord_001"
}
无需跳转——收费已静默完成认证。
{
"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"
}
将买家重定向至 url_3ds,然后继续第 4 步。
{
"uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "error",
"message": [{"message": "3DS authentication failed"}],
"code": "error_threeds_authentication"
}
处理 3DS challenge(若为 `pending`)
若响应包含 url_3ds,请重定向买家。challenge 完成后,监听 postMessage:
window.addEventListener('message', function (event) {
if (event.data?.event === '3ds-auth-complete') {
const chargeUuid = event.data.chargeUuid;
// 从后端查询收费状态并更新 UI
}
});
- 您的页面重定向(或 iframe)至
confirmacion.a55.tech/charge/{uuid} - 3DS 页面在嵌套 iframe 中加载发卡行的 ACS(Access Control Server,访问控制服务器)
- 持卡人完成验证(OTP、生物识别等)
- ACS 重定向至
confirmacion.a55.tech/validate/{uuid} - A55 验证认证结果,并沿窗口链向上发送带有
3ds-auth-complete的postMessage
通过 webhook 确认
请始终将 Webhook(网络钩子)作为事实来源(source of truth)。在收费上配置 webhook_url,以便在买家关闭浏览器时仍能收到异步更新。
{
"charge_uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "paid",
"transaction_reference": "txn_ref_001"
}
端到端流程
device_info 字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
ip_address | string | 是 | 客户端公网 IP |
user_agent | string | 是 | 浏览器 user agent 字符串 |
http_accept_content | string | 是 | HTTP Accept 头 |
http_accept_browser_value | string | 是 | 浏览器 Accept 值 |
http_browser_language | string | 是 | 浏览器语言(例如 pt-BR) |
http_browser_java_enabled | boolean | 是 | 是否启用 Java 插件 |
http_browser_javascript_enabled | boolean | 是 | 是否启用 JavaScript |
http_browser_color_depth | string | 是 | 屏幕色深 |
http_browser_screen_height | string | 是 | 屏幕高度(像素) |
http_browser_screen_width | string | 是 | 屏幕宽度(像素) |
http_browser_time_difference | string | 是 | 相对 UTC 的偏移(分钟) |
device_id | string | 否 | 稳定设备标识 |
session_id | string | 否 | 结账会话 ID |
不完整或合成的 device_info 会提高 challenge 率与拒绝率。请在客户端采集真实浏览器值并传给服务器,切勿硬编码这些值。
SDK DDC(A55Pay.authentication)
使用 JavaScript SDK 时,可单独运行 DDC:
A55Pay.authentication({
transactionReference: 'charge-uuid-or-ref',
cardBrand: 'visa',
cardNumber: '4111111111111111',
cardExpiryMonth: '12',
cardExpiryYear: '2030',
onSuccess: (payload) => {
// payload 包含 sessionId、accessToken、referenceId
// 将 sessionId 用于收费请求
},
onError: (err) => {
// DDC 失败——继续(不使用会话)或处理错误
},
});
说明: payV2() 会在处理前自动运行 DDC。仅当您手动管理流程时才需要调用 authentication()。
验证端点
POST /api/v1/bank/public/charge/authentication/{uuid}/validate
请使用 3DS 流程中的收费 uuid。A55 会验证认证元数据并返回最终结果。
使用沙箱测试卡
| Card Number | Brand | Scenario | Expected Status |
|---|---|---|---|
4111 1111 1111 1111 | Visa | 3DS 无摩擦——通过 | confirmed |
4000 0000 0000 0101 | Visa | 3DS 需要 challenge | confirmed |
4000 0000 0000 0002 | Visa | 3DS——发卡行拒绝 | declined |
5500 0000 0000 0004 | Mastercard | 3DS 无摩擦——通过 | confirmed |
4000 0000 0000 0069 | Visa | 3DS——处理错误 | error |
希望在无 challenge 的情况下提高通过率?
Visa Data Only 通过 3DS 通道向发卡行共享增强的交易数据——无需任何 challenge 跳转。在 Square 试点中,商户在 9 个月内、约 600 万笔交易上最高获得 +646 个基点 的提升。
无摩擦。无购物车放弃。A55 现支持通过单一标志启用:data_only: true。
代价是:无责任转移(欺诈由商户承担)。在拉美高流量、低风险的场景下,这一取舍往往能带来数倍回报。