PAYATHON 2026

支付失败或处理信用卡时应使用哪个 HTTP 状态码?

支付小周

结论

当支付请求格式正确、身份验证通过,但因余额不足、信用卡过期或发卡行拒绝而无法完成时,通常最适合返回 422 Unprocessable Content,其历史名称为 Unprocessable Entity。

没有必要为每一种支付失败原因使用不同的 HTTP 状态码。可以统一返回 422,再通过 JSON 中稳定的业务错误码区分具体原因:

  • insufficient_funds:余额不足
  • card_expired:信用卡已过期
  • payment_declined:无法扣款或原因未披露

如果支付服务商明确将“拒付”视为业务冲突,也可以使用 409 Conflict,但同一套 API 必须保持一致。HTTP 没有专门表示“支付失败”的通用状态码。402 Payment Required 虽然名称与支付有关,但通常不适合作为信用卡交易失败的默认状态码。

为什么推荐 422

这类支付失败通常有几个共同点:

  1. 请求中的 JSON 语法正确;
  2. 服务器能够识别字段和参数;
  3. 用户有权发起支付;
  4. 服务器理解请求,但当前支付条件不允许完成交易。

问题出在业务处理阶段,而不是 HTTP 解析阶段。422 可以较准确地表示“请求本身有效,但服务器无法按其内容完成处理”。

HTTP 状态码用于描述错误类别。前端需要展示的具体原因、能否重试和后续操作,应通过响应体提供。

状态码选择建议

场景建议状态码说明
余额不足422请求有效,但资金条件不满足
信用卡已过期422支付方式存在,但当前不可用
发卡行拒绝且未说明原因422使用通用业务错误码,不推测具体原因
JSON 无法解析400请求语法错误
缺少必填字段或字段格式错误400 或 422两种约定都很常见,需要在 API 中统一
未登录或凭证无效401身份验证失败
已登录但无权操作该支付资源403权限不足
订单不存在404目标资源不存在
订单已支付、已取消或当前状态不允许再次付款409资源状态与操作冲突
请求过于频繁429触发限流
支付网关超时504上游服务未及时响应
服务端或支付网关发生未预期故障500 或 502系统或上游服务异常

推荐的 JSON 响应结构

余额不足:

HTTP/1.1 422 Unprocessable Content
Content-Type: application/json
{
  "error": {
    "code": "insufficient_funds",
    "message": "可用余额不足,无法完成支付。",
    "retryable": false
  }
}

信用卡过期:

HTTP/1.1 422 Unprocessable Content
Content-Type: application/json
{
  "error": {
    "code": "card_expired",
    "message": "该信用卡已过期,请更换支付方式。",
    "retryable": false
  }
}

发卡行拒绝但未提供具体原因:

HTTP/1.1 422 Unprocessable Content
Content-Type: application/json
{
  "error": {
    "code": "payment_declined",
    "message": "支付未获批准,请更换支付方式或联系发卡行。",
    "retryable": false
  }
}

error.code 应是稳定且可供程序判断的标识。message 可以本地化,但不应作为业务分支的判断依据。

服务端示例

以下 Express 示例将支付网关返回的原因映射为统一的 API 错误:

app.post("/payments", async (req, res, next) => {
  try {
    const result = await paymentGateway.charge(req.body);

    if (result.status === "declined") {
      const errors = {
        insufficient_funds: {
          code: "insufficient_funds",
          message: "可用余额不足,无法完成支付。",
          retryable: false
        },
        expired_card: {
          code: "card_expired",
          message: "该信用卡已过期,请更换支付方式。",
          retryable: false
        }
      };

      const error = errors[result.reason] ?? {
        code: "payment_declined",
        message: "支付未获批准,请更换支付方式或联系发卡行。",
        retryable: false
      };

      return res.status(422).json({ error });
    }

    return res.status(201).json({
      id: result.paymentId,
      status: "succeeded"
    });
  } catch (error) {
    next(error);
  }
});

实际项目需要分别处理网关超时、网关不可用和明确拒付,不能将所有异常都转成 422。如果上游网关返回无效响应或暂时不可用,502 Bad Gateway 或 503 Service Unavailable 通常更准确。

是否应该使用 402 Payment Required

402 Payment Required 的名称看起来很符合支付场景,但它并不是信用卡拒付、余额不足或卡片过期的通用标准映射。不同 API 可能对它有不同定义,客户端也无法只根据 402 判断具体的失败原因。

只有当 API 已经明确约定 402 的语义,而且客户端、文档和监控系统都按同一约定实现时,才适合使用它。否则,应优先使用语义明确且保持一致的 422,并通过业务错误码说明具体原因。

实施步骤

  1. 将可预期的支付业务拒绝统一映射为 422。
  2. 为每种失败原因定义稳定、机器可读的错误码。
  3. 未知拒付原因统一使用 payment_declined,不要猜测发卡行没有披露的信息。
  4. 分别处理身份验证、权限、资源状态冲突、限流和上游故障。
  5. 在 API 文档中列出状态码、业务错误码以及是否允许重试。
  6. 为重复提交设计幂等机制,避免网络重试导致重复扣款。

注意事项

  • 不要返回 200 OK,再仅用 "success": false 表示失败。这会让客户端、代理、监控和告警系统难以正确识别请求结果。
  • 不要将余额不足、卡片过期等正常业务拒绝返回为 500 Internal Server Error。这些情况不属于服务器程序异常。
  • 不要向客户端暴露完整卡号、CVV、支付网关内部响应或敏感的风控信息。
  • 日志中的支付数据也需要脱敏,并遵守适用的 PCI DSS 要求。
  • “无法扣款”既可能是业务拒绝,也可能是支付网关故障。业务拒绝可以返回 422;网关故障应根据实际情况返回 502、503 或 504。
  • 是否允许重试不能只根据 HTTP 状态码判断。余额不足和卡片过期通常不适合直接重试;网络超时可能适合重试,但需要配合幂等键和支付结果查询,避免重复扣款。

备注:内容仅供参考。