Z Zise Developers

错误码

43 个对外码。认不出的一律按 api_error 处置—— 我方不会把内部码泄漏出来,所以你收到一个不在这张表里的码时,那是我方的问题,请连同 request_id 报给我们。

code 分支,不要按 HTTP 状态码分支。 几个反直觉的:service_unavailablelimit_exceededasset_not_allowedstate_invalid 都是 400 不是 5xx

统一信封。code 分支,不要按 message 分支 —— message 是给人看的英文说明,

我方保留随时改写它的自由;code 是契约,改它才是破坏性变更。


{
  "type": "invalid_request_error",
  "code": "insufficient_balance",
  "message": "The member's available balance is not enough.",
  "request_id": "01J8X4K9…"
}

每个响应都带 X-Request-Id 响应头,错误体里也带一份 request_id工单第一句话就是它。

type → HTTP

typeHTTP一句话
invalid_request_error400入参不合法,业务前置条件不满足
authentication_error401令牌/签名这一层没过
permission_error403身份是对的,但不许做这件事
not_found404对象不存在不属于你
idempotency_error409同键异体,或首次仍在处理
rate_limit_error429超配额
api_error500我方内部错误
upstream_error502上游不可用
upstream_timeout504结果不明,可能已执行

400 里混着两类东西:「你发错了」和「这一单做不成」。二者的重试方式相反,

所以别照着 HTTP 状态码写重试策略 —— 判据只能是 code,见下表的「你该怎么处置」。

三条重试规则,只有这三条

· 同一把幂等键重试api_error(500) · upstream_error(502) · upstream_timeout(504) ·

idempotency_in_progress(409) · step_up_required(400,完成托管屏之后)。

这些情况下我方可能已经把事情做完了,换新键 == 第二笔真实付款。

· 换一把新键重试:改正入参或补足余额之后。同键重发只会把首次那个失败原样回放

(幂等的语义是同一把键得到同一个结果,不是同一把键最终会成功)。

· 不要重试request_rejected · insufficient_scope · ip_not_allowed ·

merchant_disabled · product_not_available · 全部 404 · order_not_cancellable ·

state_invalid。重试改变不了判据,只会在你的错误率和我方的日志里各留一堆噪音。

幂等回放的响应带 X-Idempotent-Replay: true,且幂等窗口是 24 小时 ——

超过之后同一把键会被当成一次全新请求受理。

哪些错误带额外字段

code额外字段
limit_exceededlimit_typesingle / daily)、limit_scopemerchant / member)。两者都可能缺省
step_up_requiredchallenge_idhosted_urlexpires_at(Unix 秒)
rate_limitedretry_after(秒),同时有 Retry-After 响应头
service_unavailableretry_after(秒)—— 只在 POST /v1/merchant/deposit-address 上有
invalid_fieldsfields(逐字段的 key + reason)、reason —— 只在 POST /v1/remit/payees 上有

收到不在本表里的 code 怎么办:当成 api_error 处置(同键重试一次,仍失败就报工单

并附 request_id)。我方内部码不直通出网,所以那要么是我方漏了登记、要么是你打到了

别的服务上 —— 两种都该让我们知道。

速查

HTTP一句话
invalid_credentials401换令牌那一步的 x-client-id / x-api-key 不对、Key 已停用、已过期,或者轮换的重叠窗口已经关闭
invalid_token401访问令牌缺失、格式不对、已过期(有效期 30 分钟),或这把 Key 已被停用/过期/换了归属
invalid_signature401签名不匹配, x-timestamp / x-nonce / x-signature 三个头缺了任意一个
signature_required401该端点必须签名。POST /v1/deposits 无论 Key 怎么配都强制签名——它是唯一一个凭空产生会员余额的入口
timestamp_out_of_range401x-timestamp(Unix 秒)与我方时钟相差超过 ±300 秒
nonce_reused401这个 x-nonce 在 5 分钟的重放窗口内已经用过
environment_mismatch400沙盒凭据打了 live 域,或反过来
insufficient_scope403这把 API Key 没有该端点要求的 scope
ip_not_allowed403调用方出口 IP 不在这把 Key 的白名单里
merchant_disabled403商户主体不可用。四种内部状态合成一个码(不存在 / 暂停 / 冻结 / 关闭)
idempotency_key_required400写端点没带 x-idempotency-key
idempotency_key_invalid400幂等键不是 UUID 形态(8-4-4-4-12 的十六进制)
quote_lock_unsupported400这个端点在成交那一刻定价,传给它的报价 id 不构成一把锁
idempotency_key_reused409同一把键此前用过,但这次的请求体或目标端点跟那次不一样
idempotency_in_progress409用这把键的第一个请求还在处理中
member_not_found404会员不存在、不属于你、已被停用——三种情况同一响应
member_context_required400这是会员作用域的端点,但没带 x-on-behalf-of
member_suspended400该会员已被停用
kyc_required400该会员的 KYC 层级低于这条业务的门槛
insufficient_balance400该会员在该资产上的可用余额不足
merchant_insufficient_funds400你的预付账户可用余额不足
merchant_custody_shortfall400确认这笔提现会把你在该资产上的托管声明扣成负数——你想确认一笔自己从未声明过的钱
product_not_available400这个产品没对你授权、已停售,或不满足开通条件
limit_exceeded400撞到了某个额度。看 limit_typesingle 单笔 / daily 日累计)与 limit_scopemerchant 你的 / member 会员的)
amount_out_of_range400金额超出该产品允许的区间 —— 低于起投/起汇额、高于单笔上限、
request_rejected400风控拒绝。这是风控唯一的对外码——不带原因、不带规则名、不带阈值、不带评分
step_up_required400这个动作需要终端用户完成强认证。响应带 challenge_idhosted_urlexpires_at
order_not_cancellable400订单已经越过了可撤销的那个点
asset_not_allowed400该资产不在你的白名单里,或平台侧未启用,或这条「币 × 网络」没有可用渠道
invalid_request400请求本身不合法:body 不是合法 JSON、必填字段缺失或为空、枚举值认不出、金额格式或小数位数不对
invalid_fields400逐字段的校验失败,响应带 fields 数组(每项含 key 与 reason)
corridor_not_supported400这条汇款走廊我方发不出去(未开通、辖区受限,或这个币种/国家组合没有可用的路由码类型)
resource_not_found404引用的对象不在这个会员名下,或根本不存在——两种同一响应
state_invalid400这个动作在对象当前状态下不成立(卡已注销、订单已终态、定期理财不可提前赎回、Webhook 投递记录不可重投……)
duplicate_resource409已经存在一条一模一样的记录(例如同一个会员重复添加同一个提现地址)
address_not_allowed400这个地址不能用于提现。三种情况同一响应:它是我方自己的充值地址、在黑名单上、或格式/链不合法
qr_code_invalid400这个收款码解不出来(格式认不出、不是我方支持的码制,或上游拒绝了它)
not_found404路径不存在,或该对象不存在/不属于你。沙盒专用端点在 live 上也一律回这个码
rate_limited429超过配额。带 retry_after(秒)与 Retry-After 响应头
api_error500我方内部错误
upstream_error502上游服务商不可用
upstream_timeout504上游超时。结果不明——我方可能已经处理完了
service_unavailable400这条线此刻暂时不可用。可能是资金侧、配置侧或上游侧的原因——刻意不区分

逐条

invalid_credentials401
含义

换令牌那一步的 x-client-id / x-api-key 不对、Key 已停用、已过期,或者轮换的重叠窗口已经关闭

典型成因

按发生频率排:① 两个头没带对 —— 换令牌用的是 x-client-id + x-api-key 两个自定义头,不是 Authorization: Bearer,也不是 body 字段;空值与「密钥不对」是同一个响应,所以「我明明填了」不排除是头名写错了。

② 把 signing_key 当成 api_key 用了 —— 一把 Key 下发两个值(验签用前者、换令牌用后者),它们在后台紧挨着。

③ 这把 Key 已停用(状态不是 active)或过了有效期。

④ 轮换之后旧凭据过了重叠窗口 —— 那是硬边界,代码里没有任何宽限。

x-client-idx-api-key 来自两把不同的 Key(一把沙盒一把 live 混着用)。整套凭据同环境、只是打错了域名时,你收到的是 environment_mismatch 而不是这条。

怎么办

不要循环重试(换令牌本身有 20 次/分钟的闸,撞上去会变成 rate_limited)。「不存在」与「密钥不对」是同一个响应,所以别用它判断 client_id 存不存在。轮换后旧凭据在 prev_valid_until 那一刻硬失效,没有宽限——切换要赶在那之前完成。

invalid_token401
含义

访问令牌缺失、格式不对、已过期(有效期 30 分钟),或这把 Key 已被停用/过期/换了归属

典型成因

令牌过期 —— 有效期 30 分钟。缓存了令牌却没按响应里的 expired_at 刷新(或按「每小时刷一次」拍脑袋定周期)是接入期最常见的一种。

② 头没带对:收 x-auth-token,也收 Authorization: Bearer <token>;把 api_key 直接当 bearer 发过来同样落这里。

③ 这把 Key 在令牌还没过期时就被停用/删除/到期了 —— Key 行每个请求实时读,吊销不等令牌过期。

④ 令牌里的商户与这把 Key 现在解析出来的商户对不上(Key 换过归属,或沙盒/live 的令牌串了 —— 后者通常先撞 environment_mismatch)。

怎么办

POST /v1/connect/token 换一把新的再重发原请求。令牌该被缓存复用 —— 每个请求都换一次的接入形态会自己撞上换令牌的限流。连续几次换完还是 401,去后台看这把 Key 的状态,别在代码里死循环。

invalid_signature401
含义

签名不匹配, x-timestamp / x-nonce / x-signature 三个头缺了任意一个

典型成因

重新序列化了 body —— 哈希必须对发出去的那串原始字节算。你的 JSON 库重排了键序、改了空格或转义方式,签名就恒不匹配,而报错长得像密钥配错。

② 签名串第二段漏了 query:它是 PATH_WITH_QUERY,/v1/x?a=1 只签 /v1/x 必错。

③ 三个签名头缺了任意一个(x-timestamp / x-nonce / x-signature)——缺头与算错值是同一个码,所以「我明明算了签名」不排除是漏发了头。

④ 用了 api_key 而不是 signing_key

⑤ 五段没有用换行连接,或 method 没大写。

⑥ 空 body 的哈希算成了 sha256("{}") 而不是 sha256("")

x-timestamp 不是数字。(是数字但用了毫秒的话落 timestamp_out_of_range,不落这里。)

怎么办

逐段核对签名串——五段换行连接,顺序不可换:METHOD(大写)、PATH_WITH_QUERY(含 query)、x-timestampx-noncehex(sha256(原始 body));HMAC-SHA256 之后 base64。三处最常见的错:① 用了 api_key 而不是 signing_key(验签用的是后者,它可取回);② 把 body 重新序列化了一遍——必须对发出去的原始字节算哈希,重排键序会让签名恒不匹配而报错长得像密钥配错;③ 空 body 的哈希是 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855。重发时必须换一个新 nonce,幂等键不变。

signature_required401
含义

该端点必须签名。POST /v1/deposits 无论 Key 怎么配都强制签名——它是唯一一个凭空产生会员余额的入口

典型成因

当前无发射点。 缺签名头在校验函数里走的是 invalid_signature,全库没有任何一条路径产出这个码。它保留在目录里以免文档与代码互相删对方 ——「POST /v1/deposits 强制签名」这条规则是真的,缺的只是一个专属的码。

怎么办

invalid_signature 那一条把签名补上。⚠ 当前实现里缺签名头走的是 invalid_signature,所以你实际收到这个码的机会很小;两者的处置完全相同,不必分开分支。

timestamp_out_of_range401
含义

x-timestamp(Unix 秒)与我方时钟相差超过 ±300 秒

典型成因

时间戳用了毫秒(13 位)—— 它是个合法数字,所以不会落进 invalid_signature,而是在这里以「差了几万年」的形式出现。这一种远多于真正的时钟问题。

② 签名机器的时钟真的漂了:容器/虚拟机里没跑 NTP,几周不重启就能漂出 5 分钟。

③ 取的是本地时区的「秒」而不是 Unix 秒(差整数个小时)。

④ 请求在你自己的队列或重试器里躺了超过 5 分钟才发出去,而 timestamp 是入队时算的。

怎么办

给签名机器装 NTP。不要用「重试时沿用上一次的 timestamp」的写法——那会让一次时钟漂移变成永久失败。修好时钟后换新 nonce 重发,幂等键不变。

nonce_reused401
含义

这个 x-nonce 在 5 分钟的重放窗口内已经用过

典型成因

① 重试时沿用了上一次的 nonce。两条纪律方向相反:重试要换 nonce、不换幂等键 —— 把这两个值绑在一起生成的接入代码,第一次重试就撞这个码。

② nonce 由请求内容算出来(哈希 body、拿订单号当 nonce),于是同一笔的每次重发都是同一个值。

③ 多进程/多容器共用同一个伪随机数种子,两台机器在同一毫秒生成了同一个值。

④ 你自己的网关做了一次透明重试,把同一份请求原样发了两遍。

窗口 5 分钟,按商户 + 环境分命名空间(沙盒与 live 各一套,互不影响)。

怎么办

每次发请求都要生成新的 nonce(一个随机 UUID 就够),包括重试。nonce 是防重放的那一半,把它固定下来等于只防篡改不防重放。⚠ 换 nonce 不等于换幂等键——重试时 nonce 必须换、幂等键必须不换。

environment_mismatch400
含义

沙盒凭据打了 live 域,或反过来

典型成因

① 配置只改了一半:base URL 换到了 live 而凭据还是沙盒那把(或反过来)。

② 令牌缓存跨环境复用 —— 同一个进程里两套环境共用一个 token 变量,沙盒换来的令牌被发到了 live 域。

③ 你自建了反向代理或自定义域来访问我方:环境判据是 Host 头(含 api-sandboxsandbox. 即沙盒),代理把 Host 改写成自己的域名之后,沙盒凭据会被当成在打 live。

怎么办

不要重试。沙盒与 live 是两套凭据、两个主体、两批数据,不存在「配一下就能通」。检查 base URL 与这把 Key 的环境是否成对。⚠ 我方在这里刻意给了明确消息而不是笼统的「凭据无效」,就是因为这个错最常被误判成「密钥抄错了」,然后花半天去查密钥。

insufficient_scope403
含义

这把 API Key 没有该端点要求的 scope

典型成因

① 勾的是受限 scope(deposits:write / cards:secure)—— 它们在建 Key 时只落进待批清单等我方审批,Key 详情页上看着已勾、实际一条都没生效。这是最容易被误判成 bug 的一种。

② 只勾了读:勾 write 会自动含同资源的 read,反过来不成立。

③ scope 键拼错 —— 目录里认不出的键在展开时直接丢弃且不报错,于是那一条静默不存在。

④ 我方某个路由漏挂了 scope 声明 —— 兜底会把它变成 403 而不是放行。你确信 scope 齐全却拿到这个码时,报工单并附 request_id

怎么办

去商户后台给这把 Key 补 scope。补完立即生效,不用等令牌过期,也不用重新换令牌——scope 每个请求都实时读 Key 行(令牌载荷里那份只作提示)。补不了就说明这条业务线没对你开通,那是 product_not_available 的范畴,找客户经理。

ip_not_allowed403
含义

调用方出口 IP 不在这把 Key 的白名单里

典型成因

① 你换了服务器、加了一台,或出网走的是会变的 NAT 池 / 无服务器运行时(出口 IP 不固定)。

② 白名单里写了 /24/16/8 之外的前缀长度(/22/26 …)——我方只认这三种,其余写法一律不匹配,而且不报错:看着配了,等于没配。

③ 出口是 IPv6:CIDR 那套按点分十进制算,IPv6 只能逐条写精确地址。

④ live 环境的 Key 建的时候强制填了白名单,而你在本机或 CI 上调试。

怎么办

不要重试——换台机器重试只会多一条安全事件。去后台把新的出口 IP 加进白名单。live 环境的 Key 强制配白名单,所以「我没配所以应该不限制」在 live 上不成立。这个错最常见的成因是你换了服务器或加了一台。

merchant_disabled403
含义

商户主体不可用。四种内部状态合成一个码(不存在 / 暂停 / 冻结 / 关闭)

典型成因

① 主体处于暂停:只读仍然通、写全部拒。现象是「查得到、写不了」,多半是账务或合规上挂着一件待办 —— 四种里最常见的一种。

② 冻结 / 关闭:读写全拒。

③ 主体根本查不到 —— 沙盒的影子主体还没建出来(第一次换沙盒令牌时才惰性创建),或这把令牌不是你这个商户的。

④ 我方新增了一档状态而这里认不出:一律取严拒绝,不兜底放行。

怎么办

不要重试,联系客户经理。⚠ 一个有用的判别信号:暂停态下只读端点仍然可用,写端点全拒——如果你的现象是「查得到、写不了」,那就是暂停而不是冻结,多半是账务或合规上有一件待办事项。

idempotency_key_required400
含义

写端点没带 x-idempotency-key

典型成因

① 写端点漏带 x-idempotency-key —— 我方每一个写端点都挂着幂等中间件,没有例外名单,所以「这个接口应该不用」不成立。

② 头名照抄了别家:Idempotency-Key 少了 x- 前缀,认不出来。

③ 键放进了 body 而不是请求头。

怎么办

所有写端点(POST / PATCH / DELETE 里动钱或建对象的那些)都必须带。键由你生成并落库,别用请求内容现算——现算的键在你重试时会跟着变,等于没有幂等。GET 不需要也不接受它。

idempotency_key_invalid400
含义

幂等键不是 UUID 形态(8-4-4-4-12 的十六进制)

典型成因

① 用「订单号 + 时间戳」拼的键 —— 它多半不是 8-4-4-4-12 的十六进制形状。

② UUID 带了大括号,或把连字符去掉了。

③ 用了 ULID / 雪花号 / base64 随机串。

⚠ 校验只看形状、不看版本位(错误消息里写的是 UUID v4,但 v1/v7 同样能过)——别把「换成 v4」当修法,形状不对才是判据。

怎么办

用标准 UUID 生成器。别自己拼「订单号 + 时间戳」——它多半不符合这个形状,而且时间戳会让重试拿到一把新键。

quote_lock_unsupported400
含义

这个端点在成交那一刻定价,传给它的报价 id 不构成一把锁

典型成因

你给一个不支持锁价的端点传了 quote_id。今天只有一处会回这个码:

POST /v1/merchant/conversions(商户自己的预付资产内部调剂)。

会员兑换不再回这个码 —— 那条线的锁价已经是真的了:

POST /v1/exchange/quoteslock: true 拿回 quote_id

POST /v1/exchange/orders 带上它就按那份报价成交(过期一律拒,

绝不回落成按新价成交)。如果你在会员兑换上收到本码,说明你打的是旧版本。

怎么办

quote_id 从请求体里去掉。商户自兑换要控制汇率风险,

GET /v1/merchant/conversion-pairs 先看价、自己判断能否接受,

再立刻下单 —— 两次调用之间的波动目前由你承担。

⚠ 会员兑换要锁价走 lock: true,别把这两条线的形态混用。

idempotency_key_reused409
含义

同一把键此前用过,但这次的请求体或目标端点跟那次不一样

典型成因

① 键按会员、按天或按订单号生成,而不是按这一次调用 —— 同一个会员的第二笔就撞上。

② 拿到失败响应后改了一个字段再用原键重发(改金额、补一个可选参数)。请求哈希只算 body、不含任何请求头 —— 改头不算变,body 改一个字节就算变。

③ 同一把键打到了另一个端点(比对里含 endpoint)。

④ 批量循环里键变量提到了循环外,于是一批请求共用一把键。

⚠ 超过 24 小时窗口之后,同一把键会被当成全新请求受理,不再给这个码。

怎么办

换一把新键。请求哈希只算 body、不含任何请求头,所以「同一份 body 打到另一个端点」和「改了一个字段」是同一类错误。这个码通常意味着你的键复用逻辑出了问题——比如按会员而不是按笔生成键。⚠ 它不表示前一笔失败了,前一笔的结果还在,拿原来那把键重发就能取回。

idempotency_in_progress409
含义

用这把键的第一个请求还在处理中

典型成因

① 你自己的两个进程/两台机器同时发了同一笔(消息队列重复投递、定时任务与人工操作撞上)。

② 首次请求超时后立刻重发,而我方那一笔还在跑 —— 动钱的端点上这很正常。

③ 首次那次的收尾写入没成功(我方侧,极少见):那把键会一直停在「处理中」,要等 24 小时窗口过去才会被当成新请求。持续几分钟拿到它就报工单,别一直重试。

怎么办

等 1~2 秒,用同一把键重试(指数退避,别打紧)。绝不要换新键——换新键就是第二笔真实业务。这个码最常见于你自己的两个进程同时发了同一笔,或首次请求超时后你立刻重发。

member_not_found404
含义

会员不存在、不属于你、已被停用——三种情况同一响应

典型成因

x-on-behalf-of 用了认不出的标识 —— 只收两种:你自己的 external_member_id,以及我方发出去的 mem_<uuid>。邮箱、手机号、去掉 mem_ 前缀的裸 uuid 一律认不出。

② 沙盒与 live 是两个商户主体、两批会员 —— 沙盒里建的人在 live 上不存在。

③ 这个会员属于另一个商户。

④ 你自己把他停用了(POST /v1/members/{id}/suspend)。

⑤ 你在商户后台把他拉黑了 —— 拉黑与停用是正交两位,都收敛到这一个响应。

⚠ 后两种是「你自己做过的处置」:查代码之前先去后台看一眼这个人的状态。

怎么办

不要重试。先确认 x-on-behalf-of 的取值:两种标识都收,一是你自己的 external_member_id,二是我方响应里发出去的 mem_<uuid>——两种之外的(比如邮箱、手机号)一律认不出。会员刚被你 POST /v1/members/{id}/suspend 停用时也是这个码。⚠ 我方刻意不区分这三种,区分开就是一个跨商户会员探测接口。

member_context_required400
含义

这是会员作用域的端点,但没带 x-on-behalf-of

典型成因

① 会员作用域的端点漏传 x-on-behalf-of —— 多见于把商户主体端点(GET /v1/merchant/balances 之类)的调用代码复制过来改。

② 头传了但值是空串或全是空格:判据是 trim 之后非空。

③ 中间的网关或服务网格把不认识的自定义头剥掉了(这种情况下签名多半也过不去,两个错会一起出现)。

怎么办

补上这个头。我方绝不会回落到某个默认会员——那类兜底一旦存在,一次漏传就是把 A 的钱动到了 B 头上。以商户主体执行的端点(如 GET /v1/merchant/balances)反过来不需要它。

member_suspended400
含义

该会员已被停用

典型成因

当前无发射点。 会员停用的判定发生在 x-on-behalf-of 解析那一步,与「不存在」「属于别的商户」「已拉黑」一起收敛成 member_not_found ——这条收敛是有意的(区分开就是一个跨商户会员探测接口)。保留在目录里以免文档与代码互相删对方;别把它写进分支逻辑。

怎么办

恢复该会员(POST /v1/members/{id}/suspendsuspended: false)后重发,换新键。⚠ 当前实现下你收不到这个码:会员作用域的停用判定发生在 x-on-behalf-of 解析那一步,统一回 member_not_found。别把这个码写进分支逻辑,按 member_not_found 处理即可。

kyc_required400
含义

该会员的 KYC 层级低于这条业务的门槛

典型成因

① 这个会员的层级本来就是 0 —— 经开放 API 建出来的会员没有任何 KYC,而多数业务线的门槛是 L1。接入期几乎全部是这一种。

累计额度触发:前几笔都过了,某一笔起突然要 KYC。它最容易被当成我方的 bug,实际是门槛表里的触发线(汇款与扫码付各有一条)。

③ 该产品要 L2(汇款个人线、部分卡产品),而会员只有 L1。

站内转账里是收款人没实名 —— 付款人资料齐全照样收到这个码,该去催的是另一个人。

⑤ 卡的持卡人资料还没齐 —— 这是缺字段,不是层级不够。

怎么办

引导终端用户补齐 KYC,通过后换新键重发。响应体刻意不带层级也不带差多少——门槛是一张表,去 GET /v1/kyc/requirements 取(它按 business 列出 required_level),别按错误响应反推。当前层级读 GET /v1/members/{id}kyc_level

insufficient_balance400
含义

该会员在该资产上的可用余额不足

典型成因

① 该会员在该资产上的可用余额确实不够 —— 「可用」不含冻结中、提现中、理财中、卡内那几桶,按总额去算就会得出「他明明有钱」的结论。

② 手续费没算进去:够付本金,不够付本金加手续费。

③ 理财赎回超过持有量 —— 扣的是理财那一桶,不是可用余额。

④ 扫码付一个可扣款资产都凑不出来:按支付设置的优先级逐个试过,每一种都不够。

⑤ 汇款重新确认时余额已不够新的总额 —— 下单那一刻够、确认那一刻不够。

POST /v1/deposits 上那一支是防御性兜底(上报只会让余额增加,正常打不到)。真在那里收到它,报工单。

怎么办

不要重试。先查 GET /v1/balances 对一遍——注意「可用」不含冻结中、提现中、理财中的部分。补足之后换新键重发(同键会把这个失败原样回放给你)。⚠ 会员余额是你上报出来的:如果你确信他有钱而我方说没有,那是你少上报了一笔 POST /v1/deposits,不是我方算错了。

merchant_insufficient_funds400
含义

你的预付账户可用余额不足

典型成因

当前无发射点。 预付不足在代码里从来不走这个码:异步订单线(汇款、发卡)进排队、当场不报错;即时成交线(扫码付、兑换、理财、提现)当场回service_unavailable。它保留在目录里以免文档与代码互相删对方 ——排查资金问题请照 action 那一条走,不要等这个码出现。

怎么办

去商户后台充值。⚠ 实际线上你更可能看到的是另外两种形态:异步订单线(汇款、发卡)在预付不足时不报错,订单进排队,去 GET /v1/merchant/pending 看队列;即时成交线(扫码付、兑换、理财、提现)当场回 service_unavailable。所以排查资金问题的第一站是 /v1/merchant/pending/v1/merchant/balances,不是错误码

merchant_custody_shortfall400
含义

确认这笔提现会把你在该资产上的托管声明扣成负数——你想确认一笔自己从未声明过的钱

典型成因

唯一发射点是 POST /v1/withdrawals/{id}/confirm(两段式提现的确认那一步)。

① 有链上入金没经 POST /v1/deposits 上报过 —— 会员的余额是从别处来的(历史数据迁过来的、我方运营调整过的),而托管声明那一侧没有对应的一笔。

② 上报时资产写错了:托管声明记在了另一个资产上。逐资产各算各的,不互相顶,所以「我总共上报得够多」不构成反驳。

③ 我方某条线少记了商户托管腿 —— 这时我方侧会有一条不变量告警,报工单并附 request_id

怎么办

不要重试这一笔,先补上报。 缺的是入金:把没经 POST /v1/deposits 上报的链上到账补齐,再回来确认这笔提现。⚠ 它与 merchant_insufficient_funds 必须分开看——那条的处置是「去充预付」,这条的处置是「你的入金上报少了」。按前者去充值,充多少都不会让这笔通过。

product_not_available400
含义

这个产品没对你授权、已停售,或不满足开通条件

典型成因

产品行本身不可用:理财产品已下架/已结束/已暂停/还没开售(四种内部状态同一个码);兑换的这个方向没启用(USDT→USD 与 USD→USDT 是两条独立记录,关一个不影响另一个);站内转账总开关关着;提现的这条「币 × 网络」没有渠道。

② 发卡这一族:BIN 用尽、库存告罄、这个国家不支持、这个卡产品没对你开、快捷申请被关掉、供应商不可用。

③ 汇款的这条线(极速 / 个人)或这个资产没对你开。

④ 个人汇款线要求该会员先有上游子账户,没有就落这里。

⚠ 商户业务线开关没开时走的是 service_unavailable,不是这条 ——两者都要找客户经理,但排查的入口不同。

怎么办

不要重试。找客户经理开通,或先用 GET /v1/merchant/lines 看你开了哪几条线。⚠ 这一页上 enabledhalted两位:后者是低水位自动停售,充值就会自动恢复;前者只能由我方开。把两者合成一位的 SDK 会把一次充值即可解决的暂停当成「这条线被关了」。

limit_exceeded400
含义

撞到了某个额度。看 limit_typesingle 单笔 / daily 日累计)与 limit_scopemerchant 你的 / member 会员的)

典型成因

会员侧的日累计额度或笔数(六条线各有一套)—— 最常见的一种,典型表现是「上午还能下单,下午就不行了」。

② 单笔上限(limit_type: single)。

POST /v1/deposits 的商户侧闸:单笔与日累计两道,limit_scope: merchant。它挡的是「一把被攻破的 Key 无限记余额」,正常业务量撞不上 ——撞上了先怀疑自己被刷了,再考虑提额。

条数上限而不是金额上限:提现地址簿、充值地址簿撞的是条数,拆小金额没有用。

⑤ 卡:持卡人数量、快捷申请次数、卡片限额。

⚠ 两个额外字段都可能缺省 —— 缺了 limit_scope 时不要默认当成会员的。

怎么办

single → 拆小金额重发(换新键)。daily当天不用再试了,次日重来。limit_scope: merchant 的额度在你的商户配置里,找客户经理调;limit_scope: member 是平台准入额度,商户改不了,可用 GET /v1/members/{id}/limits 查该会员当前的占用情况,好向终端用户解释。⚠ 错误体不带阈值,别去猜,那一页才是事实源。

amount_out_of_range400
含义

金额超出该产品允许的区间 —— 低于起投/起汇额、高于单笔上限、

或扣掉手续费后不足最小记账单位。

典型成因

低于该产品的起投/起充/起汇额 —— 接入期拿一块钱试跑的请求几乎全落这里。

② 高于该产品允许的单笔金额。它与 limit_exceeded 的单笔额度不是一回事:这条是产品的区间(改个数就能过),那条是额度(要等或要提额)。

③ 手续费把本金吃光了,或换出来不足一个最小记账单位 —— 兑换与汇款上,小额试单最容易撞。

④ 理财、站内转账、扫码付各自的 min/max 区间:它们与额度是两套配置,去调额度不会让这一笔通过。

怎么办

改金额后换新键重发。与 limit_exceeded 的区别是处置相反:

那条是额度用完了、当天不用再试;这条改个数就能过。

区间在对应产品的查询端点上(如 GET /v1/earn/products/{id}

min_amount / max_amount)。

request_rejected400
含义

风控拒绝。这是风控唯一的对外码——不带原因、不带规则名、不带阈值、不带评分

典型成因

这个会员被处置了:账户冻结 / 关闭 / 受限,或被拉黑。目前最常见的一种 ——因为风控规则默认全部停用,没人打开之前它不会命中任何东西。

② 上游对这一笔明确说了「不」(卡片充值、卡内转出、冻卡/解冻、绑卡被上游拒绝)。上游是通的,它给出的是一个结论,重试不会变

③ 风控规则命中 block —— 只有我方后台开了对应规则才可能发生。

④ 收款方被拉黑(汇款收款人、站内转账的收款会员)。

⑤ 证件号已被别处使用 —— 跨商户存在性一律收敛到这个码,不告诉你冲突在哪。

⚠ 判据一律不出境,从响应里读不出是哪一种。先在商户后台看这个会员的处置状态,那是你自己看得到的那一半。

怎么办

绝不重试。 判据不会因为再发一次而改变,重发只会再留一条记录,而记录本身会让这个会员看起来更可疑。也不要试图从响应里反推命中了什么,那些信息一律不出境。要申诉走商户后台工单,附 request_id。⚠ 会员被封禁 / 拉黑 / 账户冻结也收敛在这个码里,所以「这个人以前一直好好的」不构成矛盾。

step_up_required400
含义

这个动作需要终端用户完成强认证。响应带 challenge_idhosted_urlexpires_at

典型成因

端点本身就要强认证:新增提现地址、卡片激活、换取卡密查看票据 ——第一次调用必然收到它。这不是错误,是流程的一步。

② 票据过期(5 分钟)或已经用过(一次性),而你重发时带的是旧的 challenge_id

③ 票据绑的会员或动作与这次请求对不上 —— 一张「冻结卡片」的票据不能用来查看卡密。

④ 终端用户在托管屏上没真的走完因子校验,票据的通过位还是 0。

兑换配置里该方向要求提权 —— 本期开放 API 上一律拒绝,这一支不带 challenge_id / hosted_url(没有托管屏可走)。收到一个空手的 step_up_required 就是它:要用这个方向,得先让我方把该方向的提权关掉。

怎么办

hosted_url 打开给终端用户(我方托管屏,因子校验全在我方域内),他完成之后用同一把幂等键、同一份请求体重发,并带上 x-step-up: <challenge_id>。这是唯一一个「同键重发会真的重新执行」的错误——我方为它在幂等层上开了专门的口子。票据 5 分钟过期、一次性、绑死会员与动作,用完即失效;过期就重新发起拿一张新的。⚠ 请求体里的 step_up_passed: true 这类自证我方一律不接受,别去尝试。

order_not_cancellable400
含义

订单已经越过了可撤销的那个点

典型成因

发射点只有两个POST /v1/withdrawals/{id}/confirm/fail

① 这一笔已经被推到了另一个终态(你确认过了又来标失败,或反过来)。

② 并发:两个进程同时对同一笔发了 confirm 与 fail,输的那一边收到它。

③ 这一笔从来没进过可推进的状态。

⚠ 推到同一个终态不会给这个码 —— 那是幂等回放,响应里带 replayed: true

⚠ 汇款订单的「已越过可撤销点」走的是 state_invalid,不是这条。

怎么办

不要重试。回查订单当前状态再决定下一步。⚠ 提现的两段式确认里,并发把同一笔推进到了另一个终态时也会给这个码——先 GET 一次,如果它已经是你想要的那个状态,那就是成功,不必再动。

asset_not_allowed400
含义

该资产不在你的白名单里,或平台侧未启用,或这条「币 × 网络」没有可用渠道

典型成因

① 这个资产不在你的入金白名单里。⚠ 白名单是收紧手段:一行都没配 =全部已启用资产都能用;配了第一行之后,没列进去的一律拒。加第一行的那一刻是最容易踩的地方 —— 之前能用的资产会突然全不能用。

② 平台侧没启用这个资产。

③ 这条「币 × 网络」没有可用渠道 —— 建入金地址时最常见,多半是 network写法与我方渠道表对不上。

④ 资产代号带了后缀或别名(USDT.BSC 之类)。大小写不影响,我方统一转大写。

⑤ 卡充值或站内转账在这个资产上被单独关掉了。

怎么办

不要重试。用 GET /v1/assets 取你当前可用的资产清单(没配过白名单的商户看到的是全部已启用资产——白名单是收紧手段,不是开通手段)。要加资产找客户经理,我方加一行即可,不需要你发版。

invalid_request400
含义

请求本身不合法:body 不是合法 JSON、必填字段缺失或为空、枚举值认不出、金额格式或小数位数不对

典型成因

按发生频率排:

金额格式 —— 一律字符串定点,小数位数必须等于该资产的 ledger_scale"12.5" 在 6 位资产上不合法(要写 "12.500000");用 JSON number 传金额同样落这里。

② 必填字段缺失或是空串(external_member_idassetreferencepayee_id …)。

③ id 形状不对:带前缀的 id 剥掉前缀之后必须是我方那种形态,自造值过不去。

④ body 不是合法 JSON(写端点发了空 body、或内容其实是表单编码)。

⑤ 枚举值认不出(续期模式、收款人类型、沙盒的上游名与行为名 …)。

⑥ 两个互斥参数同时给了或都没给(如报价的 payout_amountsource_amount)。

邮箱已属于另一个商户的会员 —— 出于跨商户存在性收敛,它也落在这个通用码上,响应里不会出现「已注册」四个字。建会员时反复拿到 invalid_request 而字段都对,多半是这一种。

⚠ 响应不告诉你是哪个字段,所以接入期请逐个端点对照参数表,不要靠试。

怎么办

改正后换新键重发。这是本表里出现频率最高的码,而它不告诉你是哪个字段——所以接入期请逐个端点对照参数表,不要靠试。⚠ 金额相关的错占了很大一部分:金额一律字符串定点,小数位数 = 该资产的 ledger_scale(见 GET /v1/assets);"12.5" 在 6 位资产上是不合法的,要写 "12.500000";用 JSON number 传金额同样会落到这里。

invalid_fields400
含义

逐字段的校验失败,响应带 fields 数组(每项含 key 与 reason)

典型成因

① 收款人建档的字段级校验(POST /v1/remit/payees):IBAN、路由码、户名、用途码、地址。只有这一个端点会带 fields 数组。

提现地址抄错:格式不合法、EIP-55 校验和不对、零地址、链与资产不匹配。这几种刻意不address_not_allowed —— 它们只是抄错了一个字符,讲成「这个地址不许用」会让终端用户以为自己被封了。

③ 金额字段本身不合法(兑换 / 转账 / 理财的 amount),与 ② 同属纯语法问题。

④ 卡的限额三道校验:层级关系不对、超过产品上限、小到没意义。

⑤ 卡:邮寄地址缺失或不合规、激活三要素对不上、绑卡的卡号/有效期/CVV 不匹配。

选的收款人与本单走廊不符 —— 这是选错了收款人,不是这条走廊发不出去,所以它不归 corridor_not_supported

⚠ 除 ① 之外都不带 fields,你的解析代码必须能兜住读不到它的情况。

怎么办

fields 逐条回显给终端用户,改完换新键重发。⚠ 目前只有 POST /v1/remit/payees 会带 fields——其余端点的字段级错误统一落在 invalid_request 上,那里没有字段清单可读。所以别写「所有 400 都去读 fields」的通用逻辑,读不到时要能兜住。

corridor_not_supported400
含义

这条汇款走廊我方发不出去(未开通、辖区受限,或这个币种/国家组合没有可用的路由码类型)

典型成因

① 这条走廊我方没开通,或目的地辖区受限。

② 这个「币种 × 国家」组合没有可用的路由码类型(例如只收 IBAN 的国家,你给的是本地清算号)。

③ 这条走廊当下没有可用的资金渠道。

扫码付里是上游说这条走廊没开 —— 换十张码也一样。它不归 qr_code_invalid正是为了这一点:让终端用户一直重扫是这个码最常见的误处置。

怎么办

换一条走廊或换一种支付方式,不要重试原参数。⚠ 与 product_not_available 分开的理由就在处置上:那条是「汇款这条线没开给你」(找客户经理),这条是「汇款开着,但这个目的地不行」(换目的地或改用 SWIFT)。

resource_not_found404
含义

引用的对象不在这个会员名下,或根本不存在——两种同一响应

典型成因

body 里引用的对象不属于 x-on-behalf-of 指的那个会员 —— 收款人、订单、卡片、理财持仓都按会员归属查。会员传错了,对象自然找不到,而错的那一个是会员不是对象。

② id 用了别的对象的,或前缀多了少了(pye_ 收款人 / rmt_ 汇款单 /crd_ 卡 / ern_ 理财 …)。

站内转账的收款人跨商户:一律「查无此人」,与真的不存在同一响应。

④ 对象确实不存在(已删除的收款人、从未创建的订单)。

路径参数查不到时,多数端点回的是 not_found 而不是这条 ——两个码要分别兜住。

怎么办

不要重试。检查 id 前缀是否用对了(pye_ 收款人、rmt_ 汇款单、crd_ 卡……),以及 x-on-behalf-of 指的是不是持有该对象的那个会员。⚠ 我方刻意不区分「不存在」与「不是你的」,区分开就是一个 id 探测接口。

state_invalid400
含义

这个动作在对象当前状态下不成立(卡已注销、订单已终态、定期理财不可提前赎回、Webhook 投递记录不可重投……)

典型成因

订单已经到了终态还去动它(取消一笔已结算的汇款、赎回一笔已结束的理财、重投一条已投递的 Webhook)—— 重试与并发是它最主要的来源。

报价过期:兑换报价只活一分多钟,终端用户在确认页上多停留一会儿就过期了。接入期最容易反复撞的一种。

③ 汇款「重新确认」那一族:确认窗口过期、报价又变了、这一单根本不在待确认状态、重新确认的轮次用完了。

定期理财客户端不可提前赎回 —— 这是产品决定,不是暂时不可用,不要重试。

⑤ 卡片状态不允许这个动作:已注销、在过渡态(冻结中 / 解冻中)、处在资金保护态、有未清的罚金欠款、申请单已过期、还没发货就去激活。

⑥ 提现/汇款的状态认领失败:不在途、从未分发、没有加锁凭证、太新还不能动、已经有结算凭证了。

⚠ 响应不下发内部状态名,所以先 GET 一次那个对象再决定下一步。

怎么办

GET 一次那个对象,按它现在的状态决定下一步;不要盲目重试。⚠ 响应不下发内部状态名——内部状态机不是公开契约,你能依赖的是各端点文档里列出的那几个对外状态值。

duplicate_resource409
含义

已经存在一条一模一样的记录(例如同一个会员重复添加同一个提现地址)

典型成因

同一个会员重复添加同一个提现地址 —— 撞的是库级唯一索引,所以「先查一次再插」的接入代码在并发下两条都会试着插,其中一条落这里。

② 该会员已经有一张在办的卡申请单,又发起了一张。

⚠ 它与 idempotency_key_reused 不同:这条是业务上重复,换幂等键也一样撞

怎么办

不要重试,也不要换新键——换了还是撞同一条。列一遍已有记录,直接用那条。⚠ 这与 idempotency_key_reused 不同:这条是业务上重复,与你用了哪把幂等键无关。

address_not_allowed400
含义

这个地址不能用于提现。三种情况同一响应:它是我方自己的充值地址、在黑名单上、或格式/链不合法

典型成因

这个地址是我方自己的充值地址 —— 常见于把入金地址与提现地址接反了,或者拿一个从我方接口取回来的地址去做提现目标。

② 这个地址在黑名单上。

两种同一响应,从响应里判不出是哪一种(分开等于送出一份我方地址的探测器)。

⚠ 地址抄错(格式、校验和、零地址、链不匹配)落的是 invalid_fields,不是这条 ——收到这个码时让终端用户去检查有没有抄错字符是白费力气,那不是原因。

怎么办

换一个地址。不要试图从响应里判断是哪一种——我方合并它们正是为了不给出一份我方地址的探测器。如果终端用户坚持这个地址没问题,走工单,附 request_id。⚠ 提现地址簿是主线里唯一的地址来源,新增地址要过强认证并有冷静期,所以这个错不能靠「换个入口绕过去」

qr_code_invalid400
含义

这个收款码解不出来(格式认不出、不是我方支持的码制,或上游拒绝了它)

典型成因

唯一成因是这串码解不出来(格式认不出、不是我方支持的码制、上游拒绝了它):

① 扫的是我方不支持的那一类收款码。

② 码值在传输途中被改动:截断、混进换行或空格、被多做了一次 URL 解码。

③ 拍的是屏幕上的码,解码器读错了个别字符。

⚠ 上游说「这条走廊没开」时给的是 corridor_not_supported 而不是这条 ——那种情况下让用户重扫永远不会成功。

怎么办

让终端用户重新扫一次或换一个码。不要重试同一份码值。⚠ 同一个码在不同地区/不同码制下的支持范围不同,「上次能付」不保证这次能付。

not_found404
含义

路径不存在,或该对象不存在/不属于你。沙盒专用端点在 live 上也一律回这个码

典型成因

① 路径拼错或漏了 /v1 前缀 —— 未命中的 /v1/* 全部收敛到这里。

按路径参数查对象没查到:卡片、卡申请单、汇款单、提现单、理财单、收款人、地址簿那一条 —— 这些端点用的是这个码,不是 resource_not_found。原因通常是 id 不属于 x-on-behalf-of 指的那个会员,或者不属于你这个商户。

③ 沙盒专用端点打到了 live —— 404 而不是 403:它在生产上根本不该存在,回 403 等于承认它存在。

④ id 带错了前缀(crd_ / rmt_ / wdr_ / ern_ …)。

怎么办

不要重试。先核对路径拼写与 /v1 前缀。⚠ 沙盒端点在 live 上是 404 而不是 403——那不是权限问题,是它在生产上根本不该存在。

rate_limited429
含义

超过配额。带 retry_after(秒)与 Retry-After 响应头

典型成因

换令牌太频繁(20 次/分钟)—— 每个请求都换一次令牌的接入形态会自己撞上。令牌该缓存复用到 expired_at。接入期这一种占绝大多数。

只读端点当轮询用(1200 次/分钟):撞到它基本意味着你在把我方当自己的数据库轮询 —— 改成消费 Webhook 事件,加线程只会撞得更快。

③ 下单类 120 次/分钟、会员写 300 次/分钟、POST /v1/deposits 60 次/分钟 ——批量导入会员或补报历史入金时容易撞后两档。

绑卡试错次数用尽也回 429。那是防暴力猜卡的闸,退避没用,要换动作。

⚠ 配额按商户计不按 Key 计 —— 多建几把 Key 不会让额度变多。

怎么办

Retry-After 退避并加随机抖动(一批客户端同时醒来会把一次突发变成持续冲击)。配额按商户计而不是按 Key 计——多建几把 Key 不会让额度变多。当前的分档:POST /v1/deposits 60 次/分钟、会员写 300 次/分钟、下单类 120 次/分钟、只读 1200 次/分钟、换令牌 20 次/分钟。⚠ 撞到只读那一档基本意味着你在把我方当自己的数据库轮询——改成消费 Webhook 事件,别加线程。

api_error500
含义

我方内部错误

典型成因

我方漏登记了一个内部错误码。 没登记的码在出口处一律归到这里 ——于是一次合法的业务拒绝被讲成了内部错误。它的表现很好认:同样的参数每次都 500,重试一万次结果都一样。在同一个端点、同一类参数上稳定复现的 500 几乎都是这一种,报工单比重试有用。

② 我方某条业务路径抛了未捕获异常(依赖不可用、SQL 出错、上游返回了没预期的形状)。

③ 真正的偶发抖动 —— 少数。

怎么办

用同一把幂等键重试(我方可能已经完成了部分工作,换新键有做第二遍的风险)。退避重试两三次仍失败就报工单,必须附 request_id——那是我方定位这一次调用的唯一线索。⚠ 持续在同一个端点、同一类参数上拿到 500,多半不是抖动而是一个真实缺陷,早报早修,别自己扛着重试。

upstream_error502
含义

上游服务商不可用

典型成因

发卡上游忙或外呼暂时失败(开卡、激活、充值到卡那几步)——这是全表唯一一档「原样重试一次可能就成了」的。

② 扫码付上游 5xx、网络层失败,或报价拿不回来。

汇款收款人在上游建档失败 —— ⚠ 这是上游的问题,不是你资料的问题。我方刻意没把它归成 invalid_fields,就是为了不把你引去改一份本来就对的收款人资料。

⚠ 上游是谁一律不出网,你看不到;持续几分钟不恢复就报工单。

怎么办

用同一把幂等键重试,指数退避。持续几分钟不恢复就报工单——我方能看到是哪一家、你看不到(上游名称一律不出网,避免把我方的供应商结构变成公开信息)。

upstream_timeout504
含义

上游超时。结果不明——我方可能已经处理完了

典型成因

当前我方代码里没有任何一处发射它。 真收到 504,来源是我方前面的网关(边缘超时、连接被中断),而不是某条业务路径的判断 —— 所以它携带的信息只有一条:结果不明

它保留在目录里以免文档与代码互相删对方,也因为 action 那一条(必须用同一把幂等键重试)在网关超时上同样成立 —— 而那是整份文档里代价最高的一个错。

怎么办

必须用同一把幂等键重试,或者先拿订单查询接口查证。换新键 == 第二笔真实付款,这是整份文档里代价最高的一个错误。⚠ 幂等窗口是 24 小时:超过之后同键重投 == 换新键,那时候就只能靠人工对账了,别拖。

service_unavailable400
含义

这条线此刻暂时不可用。可能是资金侧、配置侧或上游侧的原因——刻意不区分

典型成因

你这条业务线没开(业务线开关缺一行 = 不可用)—— 找客户经理,充值解决不了。

低水位自动停售:即时成交的线(扫码付、兑换、理财、提现)当场给这个码,而充值之后下一轮 cron 就自动恢复。同样的情况在异步订单线(汇款、发卡)上不报错、进排队 —— 所以两边的现象完全不同。

③ 你在该资产上的预付余额不够(同上,即时成交线当场失败)。

④ 你该资产的预付账户被处置了(状态不是 normal)。

⑤ 我方侧的配置或行情缺失:结算配置没配、手续费没配、汇率暂时取不到、扫码付供应商没配。

⑥ 新建入金地址还没好(带 retry_after)—— 那是「还没好」不是「失败了」。

⚠ 前四种在会员那一端看到的都是同一句「暂时不可用」,整条链上只有你这一端解释得清他为什么余额充足却交易失败。

怎么办

注意它是 400 不是 503,按「可重试」处置但要退避,别按 4xx 一律不重试的通用规则把它扔掉。排查顺序:先 GET /v1/merchant/balances 看你的预付够不够,再 GET /v1/merchant/lines 看这条线是 enabled=false(找客户经理)还是 halted=true(充值即自动恢复)。⚠ 会员侧看到的失败文案不会暴露原因——「他自己余额充足却交易失败」这件事,整条链上只有你这一端解释得清。带 retry_after 时(新建入金地址那一处)它的含义是「还没好」而不是「失败了」,等一下再问同一条,别换条链重来。