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