绑定实体卡(三要素;不挂强认证)
代会员调用 · 必带
x-on-behalf-of
需 x-idempotency-key
把一张已经寄到用户手上、还不属于任何人的实体卡认到这个会员名下。
入参是卡面三要素:卡号 / 有效期 / CVV。
不挂强认证是刻意的 —— CVV 只印在卡背面,三要素本身就是占有权证明;
再加一道验证只会让「拿到卡的人绑不上」。真正不可逆的那一步是激活,
提权加在那里。
⚠ 三要素一个字都不落库、不进日志。 CVV 只在这一次请求的内存里比对,
比完即弃(我方存的是卡号的带密钥 HMAC,不是卡号本身)。你的服务器上
同样不该留下它们 —— 转发一次就等于你也进了 PCI 的射程。
⚠ 换绑在上游是一次性的:换错人那张卡就废了,没有第二次机会,
上游也没有解绑接口。所以「一张卡只许有一次在途/成功的换绑」是
库级约束,你这一侧不要做「先查一次再提交」的重试。
⚠ 这条接口天然是卡号爆破口,所以按人限流:一小时内 5 次真实错误
(网络抖动与被限流本身不计次)。撞上之后要等窗口过去。
status 恒为 binding —— 上游换绑是异步的,终态由回调收敛。
前置条件
- 这张卡在库存里且已发出(
shipped) - 该会员名下有一张状态为
shipped或awaiting_bind的实体卡申请单 - 会员 KYC 已通过 L1(我方要拿它去上游建用卡人)
- 一小时内的失败次数未达 5 次
请求头
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
x-on-behalf-of |
string | 必填 | 代哪个会员调用 |
请求体
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
pan |
string | 必填 | 完整卡号。空格与连字符会被剥掉;长度 12~19 位(不同卡组织不同,不要按 16 位校验)。 |
expiry |
string | 必填 | 有效期。MM/YY / MMYY / MM-YY / MMYYYY 都收,认不出按校验失败处理。 |
cvv |
string | 必填 | 卡背面安全码,3~4 位。恒定时间比对,比完即弃。 |
application_id |
string | 可选 | 这张卡对应的申请单 id(带不带 cap_ 前缀都收)。省略则由我方按该会员的待绑单自动找。 |
响应
201已受理,上游换绑处理中
{
"application_id": "cap_1b7d90c4-5e2a-4f18-83b6-0c7a4d1e9f22",
"masked_pan": "524012******7890",
"status": "binding"
}400
product_not_available 该 BIN 或产品不支持绑定 · invalid_request body 不是合法 JSON ·
idempotency_key_required / idempotency_key_invalid
⚠ 三要素对不上、卡号格式不对、被限流、这张卡已被别人绑走、
找不到待绑申请单、KYC 未过 —— 目前都以 500 api_error 返回
(内部有区分到「是有效期不对还是 CVV 不对」的码,未登记进对外目录)。
在修好之前,给终端用户的提示只能笼统写「卡面信息不正确」,
并且要自己数次数:连错 5 次会被锁一小时。409
idempotency_key_reused · idempotency_in_progress请求
curl -X POST 'https://api.zise.com/v1/cards/bind' \
-H 'x-auth-token: Bearer $TOKEN' \
-H 'x-on-behalf-of: $MEMBER_ID' \
-H 'x-idempotency-key: $IDEMPOTENCY_KEY' \
-H 'content-type: application/json' \
-d '{
"pan": "5240121234567890",
"expiry": "08/29",
"cvv": "123"
}'const res = await fetch("https://api.zise.com/v1/cards/bind", {
method: "POST",
headers: {
"x-auth-token": "Bearer $TOKEN",
"x-on-behalf-of": "$MEMBER_ID",
"x-idempotency-key": "$IDEMPOTENCY_KEY",
"content-type": "application/json",
},
body: JSON.stringify({
"pan": "5240121234567890",
"expiry": "08/29",
"cvv": "123"
}),
});
// 金额按字符串读,别让它变成 number
const data = await res.json();import requests
res = requests.post(
"https://api.zise.com/v1/cards/bind",
headers={
"x-auth-token": "Bearer $TOKEN",
"x-on-behalf-of": "$MEMBER_ID",
"x-idempotency-key": "$IDEMPOTENCY_KEY",
"content-type": "application/json",
},
json={
"pan": "5240121234567890",
"expiry": "08/29",
"cvv": "123"
},
)
# 金额用 Decimal(str(...)),不要 float
data = res.json()req, _ := http.NewRequest("POST", "https://api.zise.com/v1/cards/bind",
strings.NewReader(`{
"pan": "5240121234567890",
"expiry": "08/29",
"cvv": "123"
}`))
req.Header.Set("x-auth-token", "Bearer $TOKEN")
req.Header.Set("x-on-behalf-of", "$MEMBER_ID")
req.Header.Set("x-idempotency-key", "$IDEMPOTENCY_KEY")
req.Header.Set("content-type", "application/json")
res, err := http.DefaultClient.Do(req)
// 金额字段用 string 接,不要 float64HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://api.zise.com/v1/cards/bind"))
.header("x-auth-token", "Bearer $TOKEN")
.header("x-on-behalf-of", "$MEMBER_ID")
.header("x-idempotency-key", "$IDEMPOTENCY_KEY")
.header("content-type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString("""
{
"pan": "5240121234567890",
"expiry": "08/29",
"cvv": "123"
}
"""))
.build();
// 金额字段用 String / BigDecimal,不要 double$ch = curl_init('https://api.zise.com/v1/cards/bind');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'x-auth-token: Bearer $TOKEN',
'x-on-behalf-of: $MEMBER_ID',
'x-idempotency-key: $IDEMPOTENCY_KEY',
'content-type: application/json',
],
CURLOPT_POSTFIELDS => <<<'JSON'
{
"pan": "5240121234567890",
"expiry": "08/29",
"cvv": "123"
}
JSON,
]);
$res = curl_exec($ch);
// 金额用 bcmath / 字符串,不要 floatval
201
{
"application_id": "cap_1b7d90c4-5e2a-4f18-83b6-0c7a4d1e9f22",
"masked_pan": "524012******7890",
"status": "binding"
}