/v1/withdraw-addresses
新增提现地址(挂强认证 + 24 小时冷静期)
withdrawals:write
代会员调用 · 必带 x-on-behalf-of
需 x-idempotency-key
链上地址是提现里唯一由用户自由输入、且错了不可挽回的东西。
银行账号打错钱会退回来,链上地址打错就是没了。所以这里三件事一起做,
不做二选一:强认证 + 冷静期 + 下单时把地址快照进订单行。
强认证走我方托管屏,商户不能自证
请求体里的 step_up_passed: true 这种形态一律不做 ——
那等于把闸门交给对端。交互是两趟:
1. 第一次调用(不带 x-step-up)→ 400 step_up_required,
响应里带 challenge_id / hosted_url / expires_at(Unix 秒,
有效期 300 秒);
2. 把 hosted_url 给终端用户打开,他在我方页面上过真实因子校验;
3. 你用同一把 x-idempotency-key、同一份请求体重发,
并带上 x-step-up: <challenge_id>。
⚠ 第 3 步之所以能成立,是因为幂等层对 step_up_required 这一种
400 刻意不做回放(request_hash 只算 body、不含请求头,
带不带 x-step-up 算出来的哈希一模一样)。除它以外的任何错误,
同键重发都会把首次那个错误原样还给你 —— 那时要换新键。
票据是一次性的,且三重绑定(绑会员、绑动作、限时)。
用完即废,不要缓存复用。
24 小时冷静期不可由商户跳过
冷静期长度由渠道决定(withdraw_channels.cooling_hours,
缺省 24 小时),不由你传参,也没有任何参数能缩短它。
它存在的全部意义是给那三条通知(邮件 + 推送 + 站内信)时间被看到 ——
那是整套地址安全模型里唯一能让终端用户自己发现异常的环节,
而你的服务器不在那条通路上。所以我方也不会把地址新增这件事
作为 Webhook 事件外发给你。
七道校验的顺序(任一不过即拒,不会落库)
条数上限 → 本地格式与 EIP-55 校验和 → 我方自己的充值地址 →
全平台黑名单 → 上游校验(挂了放行并留痕)→ 唯一索引 → 三条通知。
⚠ 未知网络一律拒:我方不认识那条链的地址规则时放行,
等于把校验完全交给用户的手指。
前置条件
- 已拿到一张通过的强认证票据(第一次调用必然拿不到,见正文两趟交互)
- 该资产与网络组合上存在启用的提现渠道
- 该会员的地址条数未达上限(缺省 20 条)
请求头
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
x-on-behalf-of |
string | 必填 | 给哪个会员加地址 |
x-idempotency-key |
string | 必填 | UUID。强认证那一趟必须沿用同一把键,见正文。 |
x-step-up |
string | 可选 | 上一趟拿到的 challenge_id。首次调用不带(也带不了)。
票据必须已由终端用户在托管屏上完成校验,否则视同没带。 |
请求体
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
asset |
string | 可选 | 资产代码。留空默认 USDT(这是唯一一个有隐含默认值的
字段,别依赖它,显式传)。 |
network |
string | 必填 | 网络机读码,如 BSC。大小写敏感。
未知网络一律拒绝(当前只有 EVM 系那几条)。 |
address |
string | 必填 | 链上地址。EVM 系必须是 0x + 40 位十六进制,
且混合大小写时 EIP-55 校验和必须对得上
(全小写或全大写放行)。落库前会归一化成 EIP-55 形态,
响应里返回的就是归一化之后的那一串。 |
label |
string | 可选 | 备注名,超过 40 字符会被截断(不报错)。 |
响应
{
"id": "wad_3c81f0d2-9a44-4d17-8e0b-2f6a1c9d4e77",
"address": "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
"usable_at": "2026-08-13T09:20:00Z",
"cooling_hours": 24,
"upstream_valid": 1
}step_up_required 需要强认证,响应带 challenge_id /
hosted_url / expires_at(这是流程的一步,不是错误) ·
invalid_request 请求体不是合法 JSON ·
invalid_fields 缺 network 或 address ·
address_not_allowed 格式不对 / 是我方自己的充值地址 /
在全平台黑名单里(三种同一响应,不要据它反推是哪一种) ·
limit_exceeded 该会员的地址条数已达上限 ·
product_not_available 这个资产与网络的组合上没有可用渠道duplicate_resource 这条地址已经在簿子里了 ·
idempotency_key_reused · idempotency_in_progressapi_error + 500 返回(内部码没登记对外映射)。
不要对它做无限重试 —— 同样的地址重试多少次都不会成功,
先把地址本身检查一遍。curl -X POST 'https://api.zise.com/v1/withdraw-addresses' \
-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 '{
"asset": "USDT",
"network": "BSC",
"address": "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
"label": "我的冷钱包"
}'
const res = await fetch(
"https://api.zise.com/v1/withdraw-addresses",
{
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({
"asset": "USDT",
"network": "BSC",
"address": "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
"label": "我的冷钱包"
}),
},
);
// 金额一律按字符串读,不要 JSON.parse 成 number
const data = await res.json();
import requests
res = requests.post(
"https://api.zise.com/v1/withdraw-addresses",
headers={
"authorization": "Bearer $TOKEN",
"x-zise-merchant": "$MERCHANT_ID",
"x-on-behalf-of": "$MEMBER_ID",
"x-idempotency-key": "$(uuidgen)",
"content-type": "application/json"
},
json={
"asset": "USDT",
"network": "BSC",
"address": "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
"label": "我的冷钱包"
},
)
# 金额用 Decimal(str(...)),不要 float
data = res.json()