支付失败或处理信用卡时应使用哪个 HTTP 状态码?
结论
当支付请求格式正确、身份验证通过,但因余额不足、信用卡过期或发卡行拒绝而无法完成时,通常最适合返回 422 Unprocessable Content,其历史名称为 Unprocessable Entity。
没有必要为每一种支付失败原因使用不同的 HTTP 状态码。可以统一返回 422,再通过 JSON 中稳定的业务错误码区分具体原因:
insufficient_funds:余额不足card_expired:信用卡已过期payment_declined:无法扣款或原因未披露
如果支付服务商明确将“拒付”视为业务冲突,也可以使用 409 Conflict,但同一套 API 必须保持一致。HTTP 没有专门表示“支付失败”的通用状态码。402 Payment Required 虽然名称与支付有关,但通常不适合作为信用卡交易失败的默认状态码。
为什么推荐 422
这类支付失败通常有几个共同点:
- 请求中的 JSON 语法正确;
- 服务器能够识别字段和参数;
- 用户有权发起支付;
- 服务器理解请求,但当前支付条件不允许完成交易。
问题出在业务处理阶段,而不是 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,并通过业务错误码说明具体原因。
实施步骤
- 将可预期的支付业务拒绝统一映射为
422。 - 为每种失败原因定义稳定、机器可读的错误码。
- 未知拒付原因统一使用
payment_declined,不要猜测发卡行没有披露的信息。 - 分别处理身份验证、权限、资源状态冲突、限流和上游故障。
- 在 API 文档中列出状态码、业务错误码以及是否允许重试。
- 为重复提交设计幂等机制,避免网络重试导致重复扣款。
注意事项
- 不要返回
200 OK,再仅用"success": false表示失败。这会让客户端、代理、监控和告警系统难以正确识别请求结果。 - 不要将余额不足、卡片过期等正常业务拒绝返回为
500 Internal Server Error。这些情况不属于服务器程序异常。 - 不要向客户端暴露完整卡号、CVV、支付网关内部响应或敏感的风控信息。
- 日志中的支付数据也需要脱敏,并遵守适用的 PCI DSS 要求。
- “无法扣款”既可能是业务拒绝,也可能是支付网关故障。业务拒绝可以返回
422;网关故障应根据实际情况返回502、503或504。 - 是否允许重试不能只根据 HTTP 状态码判断。余额不足和卡片过期通常不适合直接重试;网络超时可能适合重试,但需要配合幂等键和支付结果查询,避免重复扣款。
备注:内容仅供参考。
