POST
/v1/cards/{id}/topups
充值到卡(两段异步:到卡以 webhook 为准)
Scope
cards:write
代会员调用 · 必带 x-on-behalf-of
需 x-idempotency-key
动钱 · 扣会员源资产可用余额 + 冻结商户预付;到卡由 webhook 确认
这个端点会动钱
失败处置见下方响应表。超时(504)用同一把幂等键重试——我方可能已经处理完;业务失败要换新键,同键会原样返回那次失败。
── 这个端点的成功不等于钱到卡 ──
上游 recharge 回 succeed 只表示「下单收到了」。钱真的到卡要等
card.topup.credited 这条 webhook。所以这里回的 status 绝大多数时候是
processing,你不能据此给用户放行任何东西;同理,卡的限额也必须
等到账确认之后才下发,提前放开等于限额已开而钱没到。
── 失败与「结果不明」是两件事 ──
上游明确拒绝 → 我方当场解冻,你拿到业务失败码,钱一分没少。
网络层失败/超时 → 单子落 processing 交给巡检查证,我方绝不解冻;
你这一侧也不要重试成第二笔,用同一把幂等键重发即可拿回同一张单。
── 幂等回放的是「原始结论」,不是「永远成功」──
同一把 x-idempotency-key 打进来,第一次失败的单第二次仍然回失败。
要重新发起就换一把新键。(曾经这里一律回成功,表现是「用户第二次点
显示成功、钱没到卡、流水上写着失败」。)
⚠ 扣的是会员的可用余额,同时冻结你的预付备付金。你的预付不够时,
会员看到的只是「暂时不可用」——不许把原因转达给终端用户。
前置条件
- 卡状态在可充值集合内(
active/frozen/pending;过渡态一律不行) - 会员不在资金保护态(限额与上游失步时我方会先锁住这条路)
- 会员该资产可用余额 ≥
customer_total - 你的预付余额够付这一单的批发成本
路径参数
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
id |
string | 必填 | 卡 id |
请求头
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
x-on-behalf-of |
string | 必填 | 代哪个会员调用 |
请求体
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
amount |
string | 必填 | 基准额,源资产口径的十进制串(与试算同一个口径)。实际扣款是试算里的 customer_total。USDT 为 6 位以内:"100.00" |
source_asset |
string | 可选 | 扣款资产。省略或不可用时按支付优先级自动挑(同试算:偏好而非指令)。 |
响应
201已受理。
status 取值:processing(下单成功/结果待查证/排队中,都是这一档)·
completed(共享额度卡当场结清才会出现)。看到 201 不等于到卡。{
"id": "ctp_4a8e5d21-90bc-4f37-b1e2-8d0c6a3f5719",
"status": "processing"
}400
state_invalid 卡状态不允许充值 · product_not_available 产品或供应商不可用 ·
service_unavailable 发卡线未开通 / 你的资金账户被处置 ·
idempotency_key_required / idempotency_key_invalid
⚠ 余额不足、低于最低额、超单笔上限、上游拒绝、汇率不可用,
目前都以 500 api_error 返回(内部有码,未登记进对外目录)。
对 500 的正确处置是:用同一把键重发一次确认结论,
仍是 500 就当业务失败上报,不要循环重试。409
idempotency_key_reused · idempotency_in_progress504
upstream_timeout 结果不明 —— 用同一把幂等键重试,我方可能已经处理完触发的事件
card.topup.credited— 看事件体
调用样例
curl -X POST 'https://api.zise.com/v1/cards/{id}/topups' \
-H 'authorization: Bearer $TOKEN' \
-H 'x-zise-merchant: $MERCHANT_ID' \
-H 'x-on-behalf-of: $MEMBER_ID' \
-H 'x-idempotency-key: $(uuidgen)' \
-H 'content-type: application/json' \
-d '{
"amount": "100.00",
"source_asset": "USDT"
}'
const res = await fetch(
"https://api.zise.com/v1/cards/{id}/topups",
{
method: "POST",
headers: {
"authorization": "Bearer $TOKEN",
"x-zise-merchant": "MERCHANT_ID",
"x-on-behalf-of": "MEMBER_ID",
"x-idempotency-key": "crypto.randomUUID()",
"content-type": "application/json"
},
body: JSON.stringify({
"amount": "100.00",
"source_asset": "USDT"
}),
},
);
// 金额一律按字符串读,不要 JSON.parse 成 number
const data = await res.json();
import requests
res = requests.post(
"https://api.zise.com/v1/cards/{id}/topups",
headers={
"authorization": "Bearer $TOKEN",
"x-zise-merchant": "$MERCHANT_ID",
"x-on-behalf-of": "$MEMBER_ID",
"x-idempotency-key": "$(uuidgen)",
"content-type": "application/json"
},
json={
"amount": "100.00",
"source_asset": "USDT"
},
)
# 金额用 Decimal(str(...)),不要 float
data = res.json()