Skip to Content
View as Markdown

错误

每个 4xx/5xx 响应都携带相同的 JSON 信封:

{ "code": 400, "message": "invalid_input", "details": [ { "field": "items[0].unit_price", "message": "must be a positive decimal string" } ] }
  • code — 数字形式的 HTTP 状态码(400、401、404、……)。适合做通用的 HTTP 层处理,但分支逻辑请基于 message — code 无法区分例如 invalid_input 与 payment_method_not_supported(两者都是 400)。
  • message — lower-snake-case 的 sentinel 名的小写蛇形形式(例如 INVALID_INPUT 对应 "invalid_input")。跨版本稳定 — 请基于此分支。
  • details — 仅在校验错误时出现。{ field, message } 对象数组,便于前端把错误对应到输入框。其他情况省略。

错误 body 不包含 trace ID 或时间戳。反馈问题时,请向支持团队提供响应的 Date 标头、X-RateLimit-* 标头、你的商户 ID、端点,以及请求的大致时间。

鉴权失败使用不同的形态。 上面的信封是 API 对大多数错误的返回形式。在被处理之前就被拒绝的请求 —— /b2b/v1/* 调用上缺失 / 非法的 X-Signature、未知的 X-Client-ID,或过期的 X-Timestamp —— 会以 { "error": "...", "message": "..." } 的形态返回,其中 error 是一个粗粒度 slug(unauthorized / bad_request / service_unavailable),message 携带具体信息。没有数值 code,也没有 details。请先按 HTTP 状态码分支,再读 message。示例(401):

{ "error": "unauthorized", "message": "invalid signature" }

HTTP 状态码 → 典型 code

HTTP典型 code 值含义
400INVALID_INPUT、MISSING_REQUIRED、INVALID_FORMAT、INVALID_LENGTH、INVALID_VALUE、PAYMENT_METHOD_NOT_SUPPORTED、AMOUNT_BELOW_MINIMUM请求错误 — 看 details
401INVALID_CREDENTIALS、INVALID_TOKEN、TOKEN_EXPIRED、INVALID_SIGNATURE(B2B);SESSION_EXPIRED(仅限仪表板)鉴权失败 — 密钥错误、时间戳过期、签名错误。仪表板登录相关的 code 与 B2B 集成无关。
402INSUFFICIENT_CREDIT商户预付余额已用尽 — 充值后重试
403FORBIDDEN、IP_BLOCKED密钥有效但缺少此调用所需的 scope 或 IP 许可
404NOT_FOUND、RECORD_NOT_FOUND资源不存在(或对本商户不存在)
409ALREADY_EXISTS用不同 body 重放幂等键,或状态机拒绝转换
429TOO_MANY_REQUESTS、TOO_MANY_ATTEMPTS触发限流;退避后重试
500INTERNAL_SERVER_ERROR、EXTERNAL_SERVICE_ERROR我们的问题;可安全退避重试。
503SERVICE_UNAVAILABLE下游依赖宕机。退避后重试

完整 code 参考

你可能看到的 code 值:

鉴权 (401)

  • INVALID_CREDENTIALS — API 密钥组合被拒
  • INVALID_TOKEN — token 无法解析或被篡改
  • TOKEN_EXPIRED — token 已过期
  • SESSION_EXPIRED — 仪表板会话过期
  • INVALID_SIGNATURE — B2B / Webhook 调用上的 HMAC 签名不一致

授权 (403)

  • FORBIDDEN — 已认证,但你无权执行该操作
  • IP_BLOCKED — 来自该 IP 地址的请求被阻止

未找到 (404)

  • NOT_FOUND — 通用
  • RECORD_NOT_FOUND — 给定 ID 的行缺失

冲突 (409)

  • ALREADY_EXISTS — 通用

校验 (400)

  • INVALID_INPUT — 通用;请查看 details
  • MISSING_REQUIRED — 缺失必填字段
  • INVALID_FORMAT — 值不匹配预期格式(如 UUID、URL、email)
  • INVALID_LENGTH — 值过短或过长
  • INVALID_VALUE — 值超出允许的枚举/范围
  • PAYMENT_METHOD_NOT_SUPPORTED — 该商户未启用此 provider/asset 组合
  • AMOUNT_BELOW_MINIMUM — 订单金额低于每网络或环境级别的下限。响应中的 details.floor_usd 携带配置的下限(美元),可直接展示;message 文本中也会说明。
  • INSUFFICIENT_BALANCE — 买家钱包中该支付资产的余额不足以完成转账。
  • INSUFFICIENT_GAS — 买家钱包缺少用于支付网络手续费(gas)的原生代币,无法广播转账。

支付 / 计费 (402)

  • INSUFFICIENT_CREDIT — 商户预付余额不足以覆盖该操作的网络手续费(gas)/ 平台手续费。从仪表板充值后重试

限流 (429)

  • TOO_MANY_REQUESTS — 超出 IP 限流
  • TOO_MANY_ATTEMPTS — 同一资源(如 OTP)上重复失败触发节流

服务端错误 (500)

  • EXTERNAL_SERVICE_ERROR — 第三方 provider 失败
  • INTERNAL_SERVER_ERROR — 意外错误;请联系支持团队,并附上响应的 Date 标头与 X-RateLimit-* 标头

可用性 (503)

  • SERVICE_UNAVAILABLE — 服务暂时不可用;退避后重试

校验错误 (400)

当问题是请求 body 格式错误时,details 是数组,这样你就能把错误映射回字段:

{ "code": 400, "message": "invalid_input", "details": [ { "field": "items[0].unit_price", "message": "must be a positive decimal string" }, { "field": "success_url", "message": "must be a valid https URL" } ] }

鉴权错误 (401)

INVALID_SIGNATURE 常见有三种原因:

  • secret key 错(再检查环境变量)
  • 时间戳偏移 > 5 分钟(同步 NTP)
  • 规范化字符串错(最常见:忘了 \n 分隔符,或对解析后又重新序列化的 body 签名 — 与你实际发送的不一致)

精确签名算法见 身份验证。

限流 (429)

表面限制
所有 API 路由(含 /b2b/v1/*)按 IP 地址限流 — 500 req/min,共享桶。非按商户的配额。
公共结账 (/checkout/*)更严格的子桶 — 每 IP 20 req/min

X-RateLimit-Limit 与 X-RateLimit-Remaining 会包含在每个被限流的响应中,不仅是成功响应。请将其视为你 IP 的实时预算。

被限流的响应会包含 Retry-After 标头(距离限流窗口重置的秒数)——请遵循它。作为兜底,请带抖动退避 — 1s 基数 + 指数到 30s。

限流可能调整。如果你的正常流量触达了限流(例如对账大段历史数据),请 联系支持团队。

余额不足 (402)

INSUFFICIENT_CREDIT(HTTP 402 Payment Required)表示你的预付余额 无法覆盖所执行操作的网络手续费(gas)和平台手续费,典型场景是结算加密 支付,或执行网络手续费由平台代付的链上动作。请从商户仪表板充值 (Billing → Add credit),然后重试该操作。已在进行中的操作会在余额 充值后自动继续。

服务端错误 (5xx)

500 表示我们无法处理该请求。带退避重试 — 你的 idempotency_key 确保即使原请求已部分成功,也不会重复扣款。

如果一分钟内重试无法恢复,请向买家展示通用的”支付暂时不可用”, 并联系支持团队,附上失败的端点、你的商户 ID、响应的 Date 标头与 X-RateLimit-* 的值,以及请求的大致时间 — 这些足以让我们找到该请求。

Webhook 投递错误

Webhook 投递是一条独立的失败通道 — 它们不以 API 错误形式出现,因为 不是你的服务器在调用。当一次投递返回非 2xx(或超时),它会按指数 退避在 0s、1min、5min、15min、1h、6h 重试(总共六次 — 与 Webhooks → 概览 的时间表一致)。完整 的每次投递历史显示在仪表板的 Developers → Webhooks → [endpoint] → Delivery log。第六次尝试失败后,该投递在仪表板中被标记为 “Failed”, 你可以在服务器恢复后手动重放。