Skip to content

本章是 Payment Gateway 商户 API 的权威错误参考。用它把 HTTP 状态码与业务错误码对应到商户该采取的动作:重试、修正请求,或上报给运营人员。

响应信封

每一个商户 API 响应,无论成功或失败,都使用相同的 JSON 信封。读 code 字段,不要只看 HTTP 状态码来决定动作。

json
{
  "success": false,
  "code": "INVALID_SIGNATURE",
  "message": "X-Signature did not match expected value",
  "data": null
}
字段类型说明
successboolean2xx 时为 true,其他情况为 false
codestring机器可读。2xx 时为 SUCCESS,其他情况为下方列出的某个错误码。
messagestring人类可读。可安全写入日志。不要未翻译就直接显示给终端用户。
dataobject成功时是该操作特定的 payload。错误时为 null

部分错误响应也会包含 details 对象,附带字段层级的信息(例如 Pydantic 验证错误)。把 details 视为选用且仅供参考。

永远用 code 做分支。不要用 message 做分支:message 的文字不属于 API 合约,可能会改。

HTTP 状态码

商户 API 使用以下 HTTP 状态码。「可重试?」字段说明商户客户端是否可以在不需运营人员介入的情况下,直接重试同一个请求。

状态码含义常见原因可重试?
200OK查询成功。codeSUCCESS不需要。
201Created订单或 session 已建立。codeSUCCESS不需要。
400Bad Request业务规则拒绝了该请求,或 JSON 请求体无法解析。否。修正请求或上报给运营人员。
401UnauthorizedAPI key 缺失、无效、过期、签名错误,或时间戳超出窗口范围。否。修正凭证或签名逻辑。
403Forbidden商户停用,或缺少 can_deposit / can_withdraw 权限。否。联络平台运营人员。
404Not Found订单不存在,或订单属于不同商户。否。确认标识符是否正确。
409Conflict资源冲突,例如重复的 merchant_order_no否。先解决冲突再重试。
422Unprocessable Entity请求形状是合法的 JSON,但字段层级的验证失败。否。修正 payload。
429Too Many Requests超过速率限制。保留用途;目前并非所有端点都有强制执行。是,使用指数退避。
500Internal Server Error未处理的服务器错误。信封 code 永远是 INTERNAL_ERROR是,使用退避策略。若持续发生请开立工单。
502Bad Gateway上游渠道或出款供应商返回了无效的响应。是,使用退避策略。
503Service Unavailable服务正在启动、清空连接中,或暂时下线。是,使用退避策略。
504Gateway Timeout上游渠道超时。是,使用退避策略并带幂等键。

重试建议是针对「同一个原本送出的请求」。对 create 调用遇到 5xx 时,永远使用幂等键(即 merchant_order_no),这样重试才不会产生重复订单。

业务错误码

以下错误码会出现在信封的 code 字段。错误码是稳定的。状态码配对反映目前后端的行为。

成功

codeHTTP 状态码含义商户应采取的动作
SUCCESS200、201操作成功。继续。读取 data 取得该操作的 payload。

认证与授权

codeHTTP 状态码含义商户应采取的动作
AUTHENTICATION_ERROR401一般性认证失败。检查 X-API-Key、签名逻辑与时钟。
INVALID_SIGNATURE401X-Signature 与服务器计算出来的签名不一致。重新计算签名。检查参数排序与 api_key 附加方式。
INVALID_TIMESTAMP401X-Timestamp 缺失、格式错误,或超出 5 分钟窗口。用 NTP 同步你的时钟。以当前的 Unix 时间戳重新送出。
AUTHORIZATION_ERROR403已认证,但不被允许:商户停用,或缺少该操作的权限。联络平台运营人员。不要重试。

认证依赖会在任何业务逻辑执行前,拦截错误的 key、错误的签名与过期的时间戳。对当前请求而言,所有 401 响应都视为终局。

验证

codeHTTP 状态码含义商户应采取的动作
VALIDATION_ERROR400、422请求 payload 验证失败。422 用于 Pydantic 字段错误,会带 details.errors 数组;400 用于 service 在业务边界拒绝 payload 的情况。修正 payload。没修改就不要重试。

状态与冲突

codeHTTP 状态码含义商户应采取的动作
INVALID_STATE400目标资源目前的状态不允许此操作,例如取消一笔已完成的订单。重新查询订单看当前的 status。不要盲目重试。
CONFLICT409违反了唯一性约束,通常是重复的 merchant_order_no视为重复。决定是否用新的 merchant_order_no 重试前,先查询既有订单。
OPTIMISTIC_LOCK_ERROR400在同一笔商户余额或订单列上,与另一个同时进行的更新发生竞争。在小范围内(3 次)以短抖动重试同一请求。

OPTIMISTIC_LOCK_ERROR 是唯一一个业务层错误码,可以在不修改请求的情况下安全重试。请限制重试次数并加入抖动,避免 thundering-herd。

余额

codeHTTP 状态码含义商户应采取的动作
INSUFFICIENT_BALANCE400来源账本的一般性余额不足。充值或减少金额。不要原样重试。
INSUFFICIENT_MERCHANT_BALANCE400商户可用余额不足以涵盖一笔提款。等待充值完成入账,然后用相同金额重试,或减少金额。

找不到

codeHTTP 状态码含义商户应采取的动作
NOT_FOUND404订单不存在,或订单存在但属于不同商户。这两种情况是刻意做成无法区分的。确认你送出的标识符是否正确。

业务与渠道

codeHTTP 状态码含义商户应采取的动作
BUSINESS_ERROR400业务规则拒绝了该请求,例如「订单已过期」或「无可用充值账户」。读 message 看上下文。修正请求或上报给运营人员。不要重试同一个请求。
EXTERNAL_SERVICE_ERROR400、502上游渠道或出款供应商返回错误。用退避策略重试,并使用相同的 merchant_order_no 维持幂等性。
MERCHANT_API_ERROR400商户 API 相关失败的父类错误码。通常你会看到更具体的子类码,例如 INVALID_SIGNATURE视为终局,除非有更具体的错误码另行说明。

系统

codeHTTP 状态码含义商户应采取的动作
INTERNAL_ERROR500未处理的服务器错误。在生产环境中 message 是通用的。用退避策略重试。若持续发生,联络平台团队。

重试策略

在你的客户端决定如何处理一个响应时,使用以下这张矩阵。

HTTP 状态码可安全重试?策略
200、201N/A完成。把结果持久化。
400中止。修正请求后再送。
401中止。凭证或签名错误。重试不会解决问题。
403中止。需要运营人员介入。
404中止。标识符错误或资源不存在。
409中止。先解决冲突,例如查询既有订单。
422中止。payload 格式错误。
429带抖动的指数退避。例如限制 60 秒内最多 5 次尝试。
500指数退避。永远用相同的 merchant_order_no 维持幂等。
502指数退避。上游渠道可能会恢复。
503指数退避。服务可能正在重启。
504指数退避。使用相同的 merchant_order_no,因为订单可能已经建立。

特殊情况:遇到 OPTIMISTIC_LOCK_ERROR(HTTP 400)时,即使状态码是 400,也以短抖动重试最多 3 次。

合理的预设退避策略是 min(2^attempt * 250ms + random_jitter, 8s),上限 5 次尝试。

日志建议

记录足以调试的内容,但不要多到外泄数据。

每次错误都记录:

  • HTTP 方法、路径、状态码。
  • code,以及响应中若带有 request_id 也一并记录。
  • 你自己的 merchant_order_no,以及任何内部追踪 ID。
  • 送出的 X-Timestamp,以及你观察到的时间戳偏差。

不要记录:

  • X-API-Key 的值。只记录短前缀或哈希。
  • X-Signature 的值。只记录它验证失败的事实;值本身对调试没有用处。
  • 完整的银行账号、卡号,或终端用户 PII。写入遮罩过的值。
  • 含有以上内容的请求体。

对于 5xx 错误,从响应标头(若有)撷取 request_id 与时间戳。联络平台支援时把两者都附上。

把信封的 message 当成参考信息。不要对它做模式比对来决定逻辑分支。永远用 code 分支。

常见陷阱

以下错误是商户集成中常见的错误。每一个都对应到 API 中真实存在的失败情境。

  1. 把 HTTP 200 当成成功,不读信封内容。 商户 API 对某些查询响应会回 HTTP 200,但 success 仍然是 false。永远同时读 HTTP 状态与信封的 code

  2. 重试 401。 401 意味着你的 key、签名或时间戳错了。重试同一个 payload 只会得到同一个 401。修正客户端,再送出一个重新签名后的请求。

  3. 遇到 429 不做退避就猛打。 当速率限制有强制执行时,没有退避的反复请求只会让你持续被挡。使用带抖动的指数退避。

  4. 订单建立时忽略 5xx。 建立订单时收到 5xx 并不代表订单没有建立。服务器可能在失败前已经 commit 了订单。用相同的 merchant_order_no 重试,然后以 merchant_order_no 查询看真实状态,再决定下一步。

  5. message 文字做分支。 message 字段不属于 API 合约。新的措辞、翻译或细节变更都可能默默地让你的客户端坏掉。只用 code 做分支。

  6. 混淆 400 与 422。 400 表示业务层拒绝了一个结构上没问题的请求(例如「订单已过期」)。422 表示请求形状本身的字段验证失败。两者的修法不同:400 可能是操作面的问题,422 则一定是客户端 bug。

  7. 跨不同订单重复使用 merchant_order_no 这会产生 409 CONFLICT。每一笔新订单意图都要产生一个全新的 merchant_order_no。只有在重试「同一笔订单」时才能重复使用同一个 merchant_order_no

  8. 忽略 merchant_order_no 的重试语意。 没有稳定的幂等键,5xx 后的网络重试会建立重复订单。服务器无法替你去重。

  9. X-Timestamp 的时钟飘移。 服务器接受 5 分钟的 X-Timestamp 窗口。客户端时钟偏差超过 5 分钟会产生 INVALID_TIMESTAMP。使用 NTP。不要通过「把服务器的时钟回送过去」来「修」这个问题;用你自己当下的 Unix 时间签名。

  10. 不处理 OPTIMISTIC_LOCK_ERROR 同一商户余额的并发更新可能会输掉竞争。以短抖动重试最多 3 次。不要把第一次冲突直接上报给运营人员。

  11. 把 404 解读成「绝对不存在」。 404 也可能代表该订单属于不同商户。如果你确定自己有建立这笔订单,先回头检查 API key 与 tenant,再认定订单消失。

  12. 把 API key 或签名写进日志。 这些是凭证。一份外泄的日志文件就是一份外泄的凭证。只记录前缀或哈希。

交叉参考

  • 认证X-API-KeyX-TimestampX-Signature 是如何建立与验证的。
  • 回调 — 服务器对商户的回调如何传递状态变更;当同步 API 在 5xx 重试期间漏掉状态时,回调可以补上。
  • 沙盒 — 如何在受控环境里重现每一个错误码,再上生产环境。