本章是 Payment Gateway 商户 API 的权威错误参考。用它把 HTTP 状态码与业务错误码对应到商户该采取的动作:重试、修正请求,或上报给运营人员。
响应信封
每一个商户 API 响应,无论成功或失败,都使用相同的 JSON 信封。读 code 字段,不要只看 HTTP 状态码来决定动作。
{
"success": false,
"code": "INVALID_SIGNATURE",
"message": "X-Signature did not match expected value",
"data": null
}| 字段 | 类型 | 说明 |
|---|---|---|
| success | boolean | 2xx 时为 true,其他情况为 false。 |
| code | string | 机器可读。2xx 时为 SUCCESS,其他情况为下方列出的某个错误码。 |
| message | string | 人类可读。可安全写入日志。不要未翻译就直接显示给终端用户。 |
| data | object | 成功时是该操作特定的 payload。错误时为 null。 |
部分错误响应也会包含 details 对象,附带字段层级的信息(例如 Pydantic 验证错误)。把 details 视为选用且仅供参考。
永远用 code 做分支。不要用 message 做分支:message 的文字不属于 API 合约,可能会改。
HTTP 状态码
商户 API 使用以下 HTTP 状态码。「可重试?」字段说明商户客户端是否可以在不需运营人员介入的情况下,直接重试同一个请求。
| 状态码 | 含义 | 常见原因 | 可重试? |
|---|---|---|---|
| 200 | OK | 查询成功。code 为 SUCCESS。 | 不需要。 |
| 201 | Created | 订单或 session 已建立。code 为 SUCCESS。 | 不需要。 |
| 400 | Bad Request | 业务规则拒绝了该请求,或 JSON 请求体无法解析。 | 否。修正请求或上报给运营人员。 |
| 401 | Unauthorized | API key 缺失、无效、过期、签名错误,或时间戳超出窗口范围。 | 否。修正凭证或签名逻辑。 |
| 403 | Forbidden | 商户停用,或缺少 can_deposit / can_withdraw 权限。 | 否。联络平台运营人员。 |
| 404 | Not Found | 订单不存在,或订单属于不同商户。 | 否。确认标识符是否正确。 |
| 409 | Conflict | 资源冲突,例如重复的 merchant_order_no。 | 否。先解决冲突再重试。 |
| 422 | Unprocessable Entity | 请求形状是合法的 JSON,但字段层级的验证失败。 | 否。修正 payload。 |
| 429 | Too Many Requests | 超过速率限制。保留用途;目前并非所有端点都有强制执行。 | 是,使用指数退避。 |
| 500 | Internal Server Error | 未处理的服务器错误。信封 code 永远是 INTERNAL_ERROR。 | 是,使用退避策略。若持续发生请开立工单。 |
| 502 | Bad Gateway | 上游渠道或出款供应商返回了无效的响应。 | 是,使用退避策略。 |
| 503 | Service Unavailable | 服务正在启动、清空连接中,或暂时下线。 | 是,使用退避策略。 |
| 504 | Gateway Timeout | 上游渠道超时。 | 是,使用退避策略并带幂等键。 |
重试建议是针对「同一个原本送出的请求」。对 create 调用遇到 5xx 时,永远使用幂等键(即 merchant_order_no),这样重试才不会产生重复订单。
业务错误码
以下错误码会出现在信封的 code 字段。错误码是稳定的。状态码配对反映目前后端的行为。
成功
| code | HTTP 状态码 | 含义 | 商户应采取的动作 |
|---|---|---|---|
SUCCESS | 200、201 | 操作成功。 | 继续。读取 data 取得该操作的 payload。 |
认证与授权
| code | HTTP 状态码 | 含义 | 商户应采取的动作 |
|---|---|---|---|
AUTHENTICATION_ERROR | 401 | 一般性认证失败。 | 检查 X-API-Key、签名逻辑与时钟。 |
INVALID_SIGNATURE | 401 | X-Signature 与服务器计算出来的签名不一致。 | 重新计算签名。检查参数排序与 api_key 附加方式。 |
INVALID_TIMESTAMP | 401 | X-Timestamp 缺失、格式错误,或超出 5 分钟窗口。 | 用 NTP 同步你的时钟。以当前的 Unix 时间戳重新送出。 |
AUTHORIZATION_ERROR | 403 | 已认证,但不被允许:商户停用,或缺少该操作的权限。 | 联络平台运营人员。不要重试。 |
认证依赖会在任何业务逻辑执行前,拦截错误的 key、错误的签名与过期的时间戳。对当前请求而言,所有 401 响应都视为终局。
验证
| code | HTTP 状态码 | 含义 | 商户应采取的动作 |
|---|---|---|---|
VALIDATION_ERROR | 400、422 | 请求 payload 验证失败。422 用于 Pydantic 字段错误,会带 details.errors 数组;400 用于 service 在业务边界拒绝 payload 的情况。 | 修正 payload。没修改就不要重试。 |
状态与冲突
| code | HTTP 状态码 | 含义 | 商户应采取的动作 |
|---|---|---|---|
INVALID_STATE | 400 | 目标资源目前的状态不允许此操作,例如取消一笔已完成的订单。 | 重新查询订单看当前的 status。不要盲目重试。 |
CONFLICT | 409 | 违反了唯一性约束,通常是重复的 merchant_order_no。 | 视为重复。决定是否用新的 merchant_order_no 重试前,先查询既有订单。 |
OPTIMISTIC_LOCK_ERROR | 400 | 在同一笔商户余额或订单列上,与另一个同时进行的更新发生竞争。 | 在小范围内(3 次)以短抖动重试同一请求。 |
OPTIMISTIC_LOCK_ERROR 是唯一一个业务层错误码,可以在不修改请求的情况下安全重试。请限制重试次数并加入抖动,避免 thundering-herd。
余额
| code | HTTP 状态码 | 含义 | 商户应采取的动作 |
|---|---|---|---|
INSUFFICIENT_BALANCE | 400 | 来源账本的一般性余额不足。 | 充值或减少金额。不要原样重试。 |
INSUFFICIENT_MERCHANT_BALANCE | 400 | 商户可用余额不足以涵盖一笔提款。 | 等待充值完成入账,然后用相同金额重试,或减少金额。 |
找不到
| code | HTTP 状态码 | 含义 | 商户应采取的动作 |
|---|---|---|---|
NOT_FOUND | 404 | 订单不存在,或订单存在但属于不同商户。这两种情况是刻意做成无法区分的。 | 确认你送出的标识符是否正确。 |
业务与渠道
| code | HTTP 状态码 | 含义 | 商户应采取的动作 |
|---|---|---|---|
BUSINESS_ERROR | 400 | 业务规则拒绝了该请求,例如「订单已过期」或「无可用充值账户」。读 message 看上下文。 | 修正请求或上报给运营人员。不要重试同一个请求。 |
EXTERNAL_SERVICE_ERROR | 400、502 | 上游渠道或出款供应商返回错误。 | 用退避策略重试,并使用相同的 merchant_order_no 维持幂等性。 |
MERCHANT_API_ERROR | 400 | 商户 API 相关失败的父类错误码。通常你会看到更具体的子类码,例如 INVALID_SIGNATURE。 | 视为终局,除非有更具体的错误码另行说明。 |
系统
| code | HTTP 状态码 | 含义 | 商户应采取的动作 |
|---|---|---|---|
INTERNAL_ERROR | 500 | 未处理的服务器错误。在生产环境中 message 是通用的。 | 用退避策略重试。若持续发生,联络平台团队。 |
重试策略
在你的客户端决定如何处理一个响应时,使用以下这张矩阵。
| HTTP 状态码 | 可安全重试? | 策略 |
|---|---|---|
| 200、201 | N/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 中真实存在的失败情境。
把 HTTP 200 当成成功,不读信封内容。 商户 API 对某些查询响应会回 HTTP 200,但
success仍然是false。永远同时读 HTTP 状态与信封的code。重试 401。 401 意味着你的 key、签名或时间戳错了。重试同一个 payload 只会得到同一个 401。修正客户端,再送出一个重新签名后的请求。
遇到 429 不做退避就猛打。 当速率限制有强制执行时,没有退避的反复请求只会让你持续被挡。使用带抖动的指数退避。
订单建立时忽略 5xx。 建立订单时收到 5xx 并不代表订单没有建立。服务器可能在失败前已经 commit 了订单。用相同的
merchant_order_no重试,然后以merchant_order_no查询看真实状态,再决定下一步。用
message文字做分支。message字段不属于 API 合约。新的措辞、翻译或细节变更都可能默默地让你的客户端坏掉。只用code做分支。混淆 400 与 422。 400 表示业务层拒绝了一个结构上没问题的请求(例如「订单已过期」)。422 表示请求形状本身的字段验证失败。两者的修法不同:400 可能是操作面的问题,422 则一定是客户端 bug。
跨不同订单重复使用
merchant_order_no。 这会产生 409CONFLICT。每一笔新订单意图都要产生一个全新的merchant_order_no。只有在重试「同一笔订单」时才能重复使用同一个merchant_order_no。忽略
merchant_order_no的重试语意。 没有稳定的幂等键,5xx 后的网络重试会建立重复订单。服务器无法替你去重。X-Timestamp的时钟飘移。 服务器接受 5 分钟的X-Timestamp窗口。客户端时钟偏差超过 5 分钟会产生INVALID_TIMESTAMP。使用 NTP。不要通过「把服务器的时钟回送过去」来「修」这个问题;用你自己当下的 Unix 时间签名。不处理
OPTIMISTIC_LOCK_ERROR。 同一商户余额的并发更新可能会输掉竞争。以短抖动重试最多 3 次。不要把第一次冲突直接上报给运营人员。把 404 解读成「绝对不存在」。 404 也可能代表该订单属于不同商户。如果你确定自己有建立这笔订单,先回头检查 API key 与 tenant,再认定订单消失。
把 API key 或签名写进日志。 这些是凭证。一份外泄的日志文件就是一份外泄的凭证。只记录前缀或哈希。