Z Zise Developers

绑定实体卡(三要素;不挂强认证

POST /v1/cards/bind scope: cards:write
代会员调用 · 必带 x-on-behalf-of x-idempotency-key

把一张已经寄到用户手上、还不属于任何人的实体卡认到这个会员名下。

入参是卡面三要素:卡号 / 有效期 / CVV。

不挂强认证是刻意的 —— CVV 只印在卡背面,三要素本身就是占有权证明;

再加一道验证只会让「拿到卡的人绑不上」。真正不可逆的那一步是激活,

提权加在那里。

三要素一个字都不落库、不进日志。 CVV 只在这一次请求的内存里比对,

比完即弃(我方存的是卡号的带密钥 HMAC,不是卡号本身)。你的服务器上

同样不该留下它们 —— 转发一次就等于你也进了 PCI 的射程。

换绑在上游是一次性的:换错人那张卡就废了,没有第二次机会,

上游也没有解绑接口。所以「一张卡只许有一次在途/成功的换绑」是

库级约束,你这一侧不要做「先查一次再提交」的重试。

⚠ 这条接口天然是卡号爆破口,所以按人限流:一小时内 5 次真实错误

(网络抖动与被限流本身不计次)。撞上之后要等窗口过去。

status 恒为 binding —— 上游换绑是异步的,终态由回调收敛。

前置条件

  • 这张卡在库存里且已发出(shipped
  • 该会员名下有一张状态为 shippedawaiting_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"
}
400product_not_available 该 BIN 或产品不支持绑定 · invalid_request body 不是合法 JSON · idempotency_key_required / idempotency_key_invalid三要素对不上、卡号格式不对、被限流、这张卡已被别人绑走、 找不到待绑申请单、KYC 未过 —— 目前都以 500 api_error 返回 (内部有区分到「是有效期不对还是 CVV 不对」的码,未登记进对外目录)。 在修好之前,给终端用户的提示只能笼统写「卡面信息不正确」, 并且要自己数次数:连错 5 次会被锁一小时。
409idempotency_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 接,不要 float64
HttpRequest 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"
}