Z Zise Developers
账户中心 › 指南

会员的高危动作要一张强认证票据 —— 谁来出,怎么用。

强认证

有几件事不可逆,光有一把 API Key 不够 —— 必须让终端用户本人当场再证明一次是他。我方的做法是:把这一步放在我方托管的一屏上完成,你不接触任何因子。

红线:我方不接受你的自证

请求体里 "step_up_passed": true、请求头里 x-verified: 1 这类字段,我方一个都不认,也永远不会加

这不是不信任你。受这道闸保护的是提现地址新增、卡片激活、查看完整卡号 CVV这几件不可逆的事 —— 一旦这道闸能由调用方自己声明通过,它就不是一道闸了,而是一个字段。你那边任何一次 SSRF、任何一个能构造请求体的注入点,都会直接变成「往任意地址提币」。

所以这条流程里你要做的只有两件事:把 URL 交给终端用户,以及拿到票据后重发

哪些动作要

动作端点action
新增提现地址POST /v1/withdraw-addresseswithdraw_address_add
查看完整卡号 / CVVPOST /v1/cards/{id}/secure-sessioncard_secure
激活实体卡POST /v1/cards/{id}/activatecard_activate:<卡 id>
挂失 / 补卡POST /v1/cards/{id}/lostcard_lost:<卡 id>
设置卡 PINPOST /v1/cards/{id}/pincard_pin:<卡 id>
扫码付款POST /v1/qrpay/paymentsqrpay_pay:<码值摘要>
兑换成交(该币对配了「需验证」时)POST /v1/exchange/ordersexchange:<FROM>:<TO>
站内转账(该商户配了「需验证」时)POST /v1/transferstransfer:<收款人>:<资产>
理财申购 / 赎回(该产品配了「需验证」时)POST /v1/earn/subscriptions · POST /v1/earn/redemptionsearn_subscribe:<产品 id> · earn_redeem:<产品 id>

⚠ 删除提现地址(DELETE /v1/withdraw-addresses/{id}不挂强认证 ——删除是往安全方向走的动作,不该设障碍。同理邮寄地址簿的增删改一律不挂:真实风险是「改默认地址 → 申请实体卡 → 卡寄给攻击者」,而那道闸挂在下单那一步。

三档「什么时候要」

  1. 恒要 —— 上表前六行。不可逆,或者直接把钱花出去。
  2. 看配置 —— 兑换 / 转账 / 理财那三行。要不要由我方后台的开关决定,而你读得到GET /v1/transfers/configGET /v1/earn/products/{id}都下发 require_step_up。提前读一次、把那一跳画进流程,比等 400 再补要顺。
  3. 扫码付:每一笔都要。 GET /v1/qrpay/configrequire_step_up恒为 true,下发它只是为了让你能提前排版。

兑换那一行 2026-08-14 之前是「一律拒,且不带 hosted_url ——

也就是配了「需验证」的币对在开放 API 上永久不可成交

现在它与其余动作走同一条托管屏两段式。如果你那边写过

「碰到这个码就提示用户联系客服」的分支,可以撤了。

交互

第一次调用(不带票据)→ 400


{
  "type": "invalid_request_error",
  "code": "step_up_required",
  "message": "Strong authentication is required. See hosted_url / challenge_id.",
  "request_id": "…",
  "challenge_id": "chl_9f2c…",
  "hosted_url": "https://…/hosted/step-up/chl_9f2c…",
  "expires_at": 1754872500
}

你把 hosted_url 交给终端用户(App 内浏览器打开、或短信/推送发给他)。那一页在我方域内,我方向他的注册邮箱发一个 6 位验证码,他填对即通过。expires_at 是 Unix 秒,有效期 5 分钟

⚠ 我方不会在他完成之后回调你。这一页不签发任何会话、不返回任何东西给你 ——它只把那张票据置成「已通过」。你那边的做法是让用户点一个「我已完成验证」,或者在页面关闭后重发。

第二次调用同一把幂等键、同一份请求体,加一个头:


POST /v1/withdraw-addresses
x-idempotency-key: <与第一次完全相同>
x-step-up: chl_9f2c…

请求体也要与第一次逐字节相同(签名要重算 —— 时间戳和 nonce 都变了)。

这是幂等表里唯一一处「同键重发应当真的执行」

平时的规矩是:同一把幂等键得到同一个结果,包括错误。第一次拿到 400,第二次拿同一把键发过去,拿回的还是那个 400 的原样回放。

强认证这条是唯一的例外,我方在服务端专门为它开了一个口子。

理由是:同键判据只算请求体、不含任何请求头,而 x-step-up 是请求头 ——带不带它算出来的哈希一模一样。不开这个口子的话,你按文档重发的那一次会命中回放分支,把那份已经过期的 step_up_required 原样回放,业务处理器根本不会执行。也就是说这条流程会在代码层面永远走不完。

所以:

票据的绑定与有效期

一张票据绑死三样,任何一样对不上都当作没有这张票(于是你会拿到一张新的step_up_required 而不是一个说明原因的错误):

绑定少了它会怎样
会员你能拿 A 完成的票据去动 B 的钱
动作一张「激活卡片」的票据能用来查看卡密
已通过标志签发即可用 —— 整条链退化成「你自己声明通过了」

外加两条:

查看卡密这一档不一样

POST /v1/cards/{id}/secure-session 过了强认证之后,返回的不是卡号,而是第二个 hosted_url


{ "issued": true, "hosted_url": "https://…/hosted/card/chl_…", "expires_at": … }

完整卡号、有效期、CVV 永不经过你的服务器。它们由我方在那一页上从上游实时拉取、只渲染给那一个浏览器,不落库、不进日志、不进缓存,60 秒后页面自动遮蔽。

你拿到的只有「票据已签发」这个事实。这是刻意的:卡密经过你的服务器一次,你就落进了持卡人数据的合规范围里。

排查

现象多半是
重发之后还是 step_up_requiredchallenge_id 换了一个新的终端用户其实没走完那一页;或票据已过 5 分钟;或 x-step-up 里填的是整条 hosted_url 而不是 challenge_id
重发拿回 409 idempotency_key_reused请求体与第一次不一致(哪怕只差一个空格),或这把键打到了另一个端点
换了新幂等键之后成功了,但对账多了一笔你换键重发了一次已经完成的操作。这一档必须沿用原键

⚠ 还有一条与平时相反的:重发成功之后,那把键不会缓存成功结果。再拿它发同一个请求,拿到的是 409 idempotency_in_progress(而不是回放那次成功),且这个状态会一直持续到 24 小时窗口结束。

所以强认证这条链上,幂等键只用于「第一次 → 带票据重发」这一对。重发拿到明确响应之后就不要再用它了;真的没拿到响应(网络断在半路),去用 GET 类端点回查结果,不要盲目再发一次。