补/换卡申请(余额不在新旧卡之间直接转)
代会员调用 · 必带
x-on-behalf-of
需 x-idempotency-key
只建单,不动钱、不外呼。冻结原卡 / 退余额 / 注销 / 发新卡由我方的
执行器按状态推进。
⚠ 余额绝不在新旧卡之间直接转 —— 上游没有卡间转账接口。唯一正确的
路径是「原卡 → 退回可用余额 → 用户自行充值新卡」。做成「自动转」会在
两个上游动作之间留一个钱不在任何一张卡上的窗口,那个窗口里出任何错
都只能人工平账。
⚠ 收不收费按「是谁的原因」分,不按「麻不麻烦」分。
安全类(pan_leaked / stolen / pin_locked / damaged /
user_request / expiring)= 原卡冻结 → 退余额 → 注销为 replaced,
收补卡费与邮费;
非安全类(lost_in_transit / not_received / production_error /
name_error)= 原卡从未到达用户手上、库存行作废,两样都不收。
⚠ 非安全类要求原卡从未激活:对一张 active 的卡报「未收到」会被拒
(走那条路会跳过退余额那一步)。
⚠ 一张卡同时只许有一个在途补卡(库级约束)。并发提交第二次会被拒,
不会静默成功。
⚠ 原卡的终态是 replaced 不是 closed —— 两者刻意分开:
closed 的含义是「用户自己销的」,覆盖掉就把来源证据抹了。
前置条件
- 原卡不在
closed/replaced/expired - 非安全类场景:原卡状态不是
active - 这张卡没有其它在途的补卡单
路径参数
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
id |
string | 必填 | 原卡 id |
请求头
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
x-on-behalf-of |
string | 必填 | 代哪个会员调用 |
请求体
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
reason |
"pan_leaked" | "stolen" | "pin_locked" | "damaged" | "user_request" | "expiring" | "lost_in_transit" | "not_received" | "production_error" | "name_error" | 必填 | 补卡场景。⚠ 这一枚举目前没有在这个端点上做白名单校验 ——
传一个不在表里的值会撞库级约束并以 500 api_error 返回。
请严格按这十个值发。 |
note |
string | 可选 | 备注,落我方留痕(截断到 200 字符)。 |
响应
201已受理。
status 恒为 requested,后续推进由我方执行器完成。{
"id": "crp_8e4a2f16-b073-4c95-a2d8-3f6e1c07b94a",
"status": "requested"
}400
state_invalid 原卡状态不允许补卡 · invalid_request body 不是合法 JSON ·
idempotency_key_required / idempotency_key_invalid
⚠ 「非安全类但原卡已激活」「已有在途补卡单」「reason 不在枚举内」
目前都以 500 api_error 返回(前两种内部有码未登记,
第三种是缺了一道白名单校验)。404
not_found 卡不存在或不在这个会员名下请求
curl -X POST 'https://api.zise.com/v1/cards/{id}/replacements' \
-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 '{
"reason": "pan_leaked",
"note": "用户报告卡号在钓鱼站被输入"
}'const res = await fetch("https://api.zise.com/v1/cards/{id}/replacements", {
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({
"reason": "pan_leaked",
"note": "用户报告卡号在钓鱼站被输入"
}),
});
// 金额按字符串读,别让它变成 number
const data = await res.json();import requests
res = requests.post(
"https://api.zise.com/v1/cards/{id}/replacements",
headers={
"x-auth-token": "Bearer $TOKEN",
"x-on-behalf-of": "$MEMBER_ID",
"x-idempotency-key": "$IDEMPOTENCY_KEY",
"content-type": "application/json",
},
json={
"reason": "pan_leaked",
"note": "用户报告卡号在钓鱼站被输入"
},
)
# 金额用 Decimal(str(...)),不要 float
data = res.json()req, _ := http.NewRequest("POST", "https://api.zise.com/v1/cards/{id}/replacements",
strings.NewReader(`{
"reason": "pan_leaked",
"note": "用户报告卡号在钓鱼站被输入"
}`))
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/{id}/replacements"))
.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("""
{
"reason": "pan_leaked",
"note": "用户报告卡号在钓鱼站被输入"
}
"""))
.build();
// 金额字段用 String / BigDecimal,不要 double$ch = curl_init('https://api.zise.com/v1/cards/{id}/replacements');
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'
{
"reason": "pan_leaked",
"note": "用户报告卡号在钓鱼站被输入"
}
JSON,
]);
$res = curl_exec($ch);
// 金额用 bcmath / 字符串,不要 floatval
201
{
"id": "crp_8e4a2f16-b073-4c95-a2d8-3f6e1c07b94a",
"status": "requested"
}