Skip to Content

错误

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

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

我们目前在 body 中返回 trace_idtimestamp。本页早期草稿曾承诺过两者 — 那是设想,并非实情。如果需要把服务器日志与一次请求对应起来,请抓取响应的 Date 标头以及 gateway 侧的限流标头(X-RateLimit-*),在工单中引用。

gateway 边缘的拒绝使用不同的形态。 上面的信封是后端服务发出的。在到达服务 之前 就被 gateway 拒绝的请求 —— /b2b/v1/* 调用上缺失 / 非法的 X-Signature、未知的 X-Client-ID,或过期的 X-Timestamp —— 会以 { "error": "...", "message": "..." } 的形态返回,其中 error 是一个粗粒度 slug(unauthorized / bad_request / service_unavailable),message 携带具体信息。没有数值 code,也没有 details。因此校验方应先按 HTTP 状态码分支,再读 message,只有当请求越过 gateway 之后才把 code / details 当作存在。gateway body 示例(401):

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

HTTP 状态码 → 典型 code

HTTP典型 code含义
400INVALID_INPUTMISSING_REQUIREDINVALID_FORMATINVALID_LENGTHINVALID_VALUEINVALID_USER_STATUSINVALID_USER_ROLEPAYMENT_METHOD_NOT_SUPPORTEDAMOUNT_BELOW_MINIMUM请求错误 — 看 details
401INVALID_CREDENTIALSINVALID_TOKENTOKEN_EXPIREDINVALID_SIGNATURE(B2B);INVALID_OTPOTP_EXPIREDSESSION_EXPIRED(仅限仪表板 / JWT)鉴权失败 — 密钥错误、时间戳过期、签名错误。OTP/SESSION_EXPIRED 这些 code 仅在仪表板 JWT 路由(/payment/v1/*)上出现;纯 B2B 集成不会看到。
402INSUFFICIENT_CREDIT商户预付余额已用尽 — 充值后重试
403FORBIDDENIP_BLOCKED密钥有效但缺少此调用所需的 scope 或 IP 许可
404NOT_FOUNDRECORD_NOT_FOUNDUSER_NOT_FOUNDSESSION_NOT_FOUND资源不存在(或对本商户不存在)
409ALREADY_EXISTSUSER_ALREADY_EXISTSSESSION_ALREADY_EXISTS用不同 body 重放幂等键,或状态机拒绝转换
429TOO_MANY_REQUESTSTOO_MANY_ATTEMPTS触发限流;退避后重试
500INTERNAL_SERVER_ERRORDATABASE_CONNECTION_ERRORDATABASE_QUERY_ERRORDATABASE_TRANSACTION_ERRORREDIS_CONNECTION_ERRORREDIS_OPERATION_ERROREXTERNAL_SERVICE_ERROR我们的问题;可安全退避重试。(TWILIO_SERVICE_ERROR / SENDGRID_SERVICE_ERROR 这类 provider 专属 code 在内部存在,但仅在仪表板侧的通知流程中出现,不会在 B2B 端点上暴露。)
503SERVICE_UNAVAILABLE下游依赖宕机。退避后重试

完整 code 参考

你可能看到的 code 完整集合(与 payment-service/pkg/errors/errors.go 一致):

鉴权与会话 (401)

  • INVALID_CREDENTIALS — 用户名/密码或 API key 组合被拒
  • INVALID_TOKEN — JWT/会话 token 无法解析或被篡改
  • TOKEN_EXPIRED — JWT 过期(超过 exp)
  • INVALID_OTP — OTP 不匹配
  • OTP_EXPIRED — OTP 签发时间超出容差窗口
  • SESSION_EXPIRED — 仪表板会话过期
  • INVALID_SIGNATURE — B2B / Webhook 调用上的 HMAC 签名不一致

授权 (403)

  • FORBIDDEN — 已认证,但角色/scope/商户边界阻止该操作
  • IP_BLOCKED — IP 在滥用名单内

未找到 (404)

  • NOT_FOUND — 通用
  • RECORD_NOT_FOUND — 给定 ID 的行缺失
  • USER_NOT_FOUND — 用户查找失败
  • SESSION_NOT_FOUND — 仪表板 session id 无法识别

冲突 (409)

  • ALREADY_EXISTS — 通用
  • USER_ALREADY_EXISTS — 注册触发唯一约束
  • SESSION_ALREADY_EXISTS — 重复 session 插入

校验 (400)

  • INVALID_INPUT — 通用;请查看 details
  • MISSING_REQUIRED — 缺失必填字段
  • INVALID_FORMAT — 值不匹配预期格式(如 UUID、URL、email)
  • INVALID_LENGTH — 值过短或过长
  • INVALID_VALUE — 值超出允许的枚举/范围
  • INVALID_USER_STATUS — 用户处于不允许该操作的状态
  • INVALID_USER_ROLE — 角色无该操作权限
  • 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 — gateway IP 级限流
  • TOO_MANY_ATTEMPTS — 同一资源(如 OTP)上重复失败触发节流

存储 / 基础设施 (500)

  • DATABASE_CONNECTION_ERROR — 无法连接数据库
  • DATABASE_QUERY_ERROR — 查询在运行时失败
  • DATABASE_TRANSACTION_ERROR — commit/rollback 失败
  • REDIS_CONNECTION_ERROR — 无法连接 Redis
  • REDIS_OPERATION_ERROR — Redis 命令失败
  • EXTERNAL_SERVICE_ERROR — 通用第三方失败(未在下方分桶的 provider)
  • TWILIO_SERVICE_ERROR — Twilio SMS / Verify 调用失败
  • SENDGRID_SERVICE_ERROR — SendGrid 邮件发送失败
  • 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)

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

X-RateLimit-LimitX-RateLimit-Remaining 每次被 gateway 限流的响应都会回显(不仅是成功响应)。请将其视为你 IP 的实时预算。

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

限流可能调整。如果你合法地撞上了限流(例如对账大段历史数据),请 联系我们 — 批量端点在路线图上。

余额不足 (402)

INSUFFICIENT_CREDIT(HTTP 402 Payment Required)表示商户预付余额 无法覆盖你试图执行的操作的下一笔 gas 和平台费 — 典型场景是结算加密 支付,或执行 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” 标签,等你服务器恢复后可以手动重放。