错误码与 HTTP 状态
HTTP 状态说明请求有没有被执行,业务结果看 status。错误响应结构、retryable 的含义、30 个错误码、按接口查看与处理示例。
最后更新于 2026-10-14 · 契约 v1.0.0
先看结论
- HTTP 状态说明请求有没有被执行:4xx 表示本次请求被拒绝、没有产生业务效果(409 表示已有相关对象);2xx 表示已处理。
- 业务结果看对象的
status:2xx 不等于扣款成功,支付成功以status=succeeded或经验签的payment.succeeded通知为准。 - 5xx 与超时是“结果未知”:请求可能已经被受理。用原幂等键重试或按商户订单号查询,绝不换新键重扣(
503 temporarily_unavailable除外:本次未受理)。
HTTP 状态
| HTTP | 含义 | 你的处理 | 是否可能已产生业务效果 | 能否用原幂等键重试 |
|---|---|---|---|---|
| 200 | 查询、确认或幂等复用已完成的响应 | 检查业务状态 | 是,以 status 为准 | 不需要 |
| 201 | 成功创建业务对象 | 不代表支付必然成功,继续检查 status | 是 | 不需要 |
| 202 | 平台已受理,结果待确认 | 保存对象 ID,查询或等待通知 | 可能,结果未知 | 可以(返回同一对象的最新状态),但应优先等通知或查询 |
| 400 | 参数缺失、格式错误等 | 修正参数,不自动重扣 | 否 | 修正后用新键重新提交 |
| 401 | 密钥无效或失效 | 检查凭据 | 否 | 修正凭据后可重试 |
| 403 | 权限、商户能力或环境不允许 | 检查权限,不绕过限制 | 否 | 先解决权限 |
| 404 | 对象不存在或不属于当前商户环境 | 核对 ID 与所用 Key 的环境;平台不暴露其他商户对象是否存在 | 否 | — |
| 409 | 幂等冲突、订单冲突、在途尝试或状态冲突 | 查询已有对象(related_resource_id)后决定下一步 | 已有相关对象存在 | 视错误码(operation_in_progress 可以;其余不可) |
| 422 | 币种不支持、金额不符、退款资金不足等业务校验失败 | 根据错误码修正业务 | 否 | 修正后重新提交(用新键) |
| 429 | 请求受限,尚未按本次新请求执行 | 按 Retry-After 和原幂等键退避重试 | 否 | 是,等待 Retry-After 秒后 |
| 5xx | 平台或依赖异常 | 可能已受理;同键重试或按业务号查询,不能换键猜测重扣 | 未知(503 temporarily_unavailable 除外:未受理) | 是;或按商户订单号 / 退款号查询 |
- 网络超时、连接中断、拿不到响应,与 5xx 同样处理:结果未知。
503 temporarily_unavailable是唯一明确表示“未受理”的 5xx:平台暂停了该通道的新交易,本次请求没有执行、没有产生 Payment 或扣款,稍后用原幂等键重试即可;响应不说明内部原因。其他 5xx 一律按结果未知处理。- 5xx 响应可能不是 JSON(例如网关返回的 HTML),解析失败也按“结果未知”处理。
- 平台已经受理支付后,不会因为上游超时把它伪装成 429;已受理的请求返回 202
processing。
错误响应结构
| 字段 | 类型 | 说明 |
|---|---|---|
error.code | string | 稳定的机器可读错误码,按它编程;须容忍未知错误码 |
error.message | string | 面向开发者的脱敏说明,不含卡数据或密钥;文案可能调整,不要解析它 |
error.field | string,可为空 | 出错的请求字段(如 amount、browser_info.ip_address、Idempotency-Key);与字段无关时为 null |
error.related_resource_id | string,可为空 | 相关的已有对象 ID,如冲突的 Payment pay_…;用它查询后决定下一步 |
error.retryable | boolean | 是否适合以原幂等键重试当前请求(见下一节) |
request_id | string | 本次请求的唯一标识;排查与联系技术支持时提供 |
{
"error": {
"code": "payment_in_progress",
"message": "该订单已有未完成的支付尝试,请查询原支付结果。",
"field": null,
"related_resource_id": "pay_demo_001",
"retryable": false
},
"request_id": "req_demo_error"
}说明:400 / 422 修正后用哪个幂等键
被 400、401、403、422 拒绝的请求没有执行业务,也不占用幂等键;修正参数后使用新的幂等键重新提交。这一规则只适用于 400 / 401 / 403 / 422,409、429、5xx 不适用。
retryable 的含义
retryable=true只表示“适合用原来的幂等键重新发送同一个请求”,例如rate_limited、operation_in_progress、internal_error、temporarily_unavailable。- 它不授权换新的幂等键、换新的商户订单号或“再创建一笔支付”。结果未知时换键重试可能造成重复扣款。
retryable=false不代表“这笔支付失败了”:例如payment_in_progress是 false,因为正确做法是查询已有支付,而不是重发。- 上游明确拒绝扣款时不返回错误,而是返回 Payment,
status=failed并带failure,见支付失败原因与 API 错误的区别。
警告:结果未知时不要换幂等键或订单号重新扣款
超时、5xx、202 都可能已经扣款;用同一个幂等键重试,或按订单号查询。详见结果未知时如何查单。
错误码字典
按 error.code 编程,并容忍将来新增的错误码(按 HTTP 状态处理)。每行可通过 #code-<错误码> 直接链接,例如 #code-payment_in_progress。
| 错误码 | HTTP | 含义 | 常见原因 | 处理建议 | 可用原键重试 |
|---|---|---|---|---|---|
| invalid_request示例:merchant_order_no 格式不正确 | 400 | 参数缺失或格式错误 | 缺 Idempotency-Key;订单号含非法字符;payment_method_data 与 payment_method_id 同时提交;confirm=false 携带卡信息;confirm=true 缺 return_url;收银会话两组身份参数同时提交或都缺;缺少上游要求的 browser_info / billing_details 字段;游标、时间格式、limit 不合法 | 按 error.field 修正后重新提交,不要自动重扣 | 否 |
| sensitive_data_rejected示例:description 中疑似包含卡号等敏感信息 | 400 | 自由文本中疑似包含卡号等敏感信息 | description、metadata 中出现卡号样式的数字等 | 删除敏感内容后重新提交;检查你的系统为何把卡数据写进这些字段 | 否 |
| unauthenticated示例:API Key 无效或已失效 | 401 | API Key 缺失、无效、已撤销或已过期 | 未带 Authorization;Key 复制不完整;轮换后旧 Key 已过期;Key 被撤销 | 检查凭据,在 商户后台 API Key 页面 核对 Key 状态与环境 | 否 |
| forbidden示例:API Key 缺少该操作所需的权限范围 | 403 | Key 的权限范围不允许该操作 | 只读 Key 创建退款;getEvent 缺少事件所属业务的读权限;请求来源 IP 不在 Key 的白名单时返回更具体的 ip_not_allowed | 使用具备所需 scope 的 Key;不要绕过限制 | 否 |
| ip_not_allowed示例:请求来源 IP 不在该 API Key 的白名单内 | 403 | 请求来源 IP 不在该 API Key 的 IP 白名单内 | Key 配置了 IP 白名单,请求来自未登记的服务器 IP | 在商户后台 API Key 页面核对白名单,或从已登记的服务器发起请求 | 否 |
| capability_disabled示例:商户未开通直接传卡 | 403 | 商户未开通或已关闭对应能力 | 未开通直接传卡;支付或退款申请能力被关闭;该币种未开放 | 改用托管收银台或联系平台运营 | 否 |
| merchant_not_active示例:商户当前不受理新业务 | 403 | 商户不是已启用状态,不受理新业务 | 商户被暂停或关闭;已受理业务的回调、查询、通知不受影响 | 联系平台运营 | 否 |
| not_found示例:对象不存在或不属于当前商户与环境 | 404 | 对象不存在或不属于当前商户与环境 | ID 错误;用测试 Key 查询生产对象;查询平台异常退款(不通过开放 API 返回) | 核对 ID 与 Key 的环境;平台不会透露对象是否属于其他商户 | 否 |
| idempotency_conflict示例:相同的 Idempotency-Key 已用于内容不同的请求 | 409 | 同一幂等键被用于安全业务参数不同的请求 | 复用了键但金额、币种、订单号等不同 | 属于集成缺陷:检查幂等键生成逻辑;不要改参数后重放原键;新操作使用新键 | 否 |
| operation_in_progress示例:相同的请求正在处理中,请稍后用原幂等键重试 | 409 | 同一幂等键的请求仍在执行 | 并发发送了同键请求 | 稍后用同一个键重试,或查询 related_resource_id;不要换键 | 是 |
| payment_in_progress示例:该订单已有未完成的支付尝试,请查询原支付结果 | 409 | 该订单已有未完成的支付尝试 | 在途(processing / requires_action)时再次 confirm,或用新键对同一订单重复提交 | 查询 related_resource_id 指向的 Payment,按其状态继续(引导付款人完成认证或等待结果);不要新建订单 | 否 |
| order_no_conflict示例:商户订单号已对应一个 Payment | 409 | 商户订单号已对应一个 Payment | 换幂等键但商户订单号相同 | 查询 related_resource_id;确需再次收款且原支付已关闭时使用新订单号 | 否 |
| refund_no_conflict示例:商户退款号已被使用 | 409 | 商户退款号已被使用 | 换幂等键重复提交同一退款号 | 查询 related_resource_id 指向的退款 | 否 |
| invalid_state示例:支付当前状态不允许该操作 | 409 | 对象当前状态不允许该操作 | 取消 processing / succeeded 的支付;对已过期、已取消、已成功的订单 confirm;为不可支付的订单创建收银会话 | 查询对象最新状态后决定 | 否 |
| attempt_limit_reached示例:该订单已达到最多尝试次数 | 409 | 该订单已达到最多尝试次数 | 默认最多 3 次,可被平台收紧 | 不能再对该订单扣款;确认原支付关闭后用新订单号 | 否 |
| refund_amount_exceeds示例:退款金额超过可申请退款本金 | 422 | 退款金额超过可申请退款本金 | 可申请退款本金 = 已确认扣款 − 已成功退款本金 − 未结束退款占用 − 争议独占本金;并发部分退款累计超额 | 查询支付的 captured_amount、refunded_amount、refund_reserved_amount 后按可退金额重新申请 | 否 |
| insufficient_refund_funds示例:可用资金不足以覆盖退款本金与退款手续费,差额 USD 3.50 | 422 | 商户可用资金不足以覆盖退款本金与退款手续费 | 资金尚未结算或已占用打款;例:支付 100.00、手续费 3.50、只有净额 96.50 时全额退款差 3.50 | message 给出差额,本次不生成退款单;线下补款并由平台财务登记后,或经平台批准动用你的保证金 / 冻结资金后(授权 7 天内有效),用同一商户退款号与金额、新的幂等键重新提交;平台不会自动缩减退款金额,也不替你垫付 | 否 |
| refund_not_allowed示例:该支付当前不能退款 | 422 | 该支付当前不能退款 | 支付未成功;超过上游允许的退款期限;交易处于争议中且上游不允许退款 | 按 message 处理;争议交易在商户后台争议页面处理 | 否 |
| currency_not_supported示例:币种未启用或不在可用范围 | 422 | 币种未启用或不在可用范围 | 商户配置与上游能力的交集不含该币种 | 改用已启用币种或联系平台运营 | 否 |
| amount_out_of_range示例:金额不在允许的范围内 | 422 | 金额不满足限额 | 低于最低 / 高于最高单笔金额,或超过日累计限额;错误中不回传具体阈值 | 调整金额或联系平台 | 否 |
| card_brand_not_supported示例:卡品牌不受支持 | 422 | 卡品牌不受支持 | 商户配置与上游能力的交集不含该品牌 | 提示付款人换卡 | 否 |
| fee_exceeds_amount示例:手续费超过交易金额 | 422 | 手续费超过交易金额 | 金额过小,按费率计算的手续费超过金额 | 提高金额或联系平台调整配置 | 否 |
| order_amount_mismatch示例:订单金额或币种与已有支付不一致 | 422 | 复用已有订单时金额或币种不一致 | 创建收银会话时同一订单号的金额或币种变了 | 金额不可修改:关闭原支付后用新订单号创建 | 否 |
| payment_method_unusable示例:支付凭据不能用于本次支付 | 422 | 支付凭据不能用于本次支付 | payment_method_id 不属于当前环境、上游账户或本商户 | 重新获取支付凭据 | 否 |
| url_not_allowed示例:return_url 不匹配已登记的回跳地址规则 | 422 | 回跳地址不合法 | return_url / cancel_url 不匹配已登记的地址规则 | 在 商户后台回跳地址页面 登记后重新提交 | 否 |
| risk_rejected示例:请求未通过风控检查 | 422 | 请求被平台风控拒绝 | 命中风险名单等基础规则;不说明具体规则与阈值 | 不要尝试绕过;有疑问联系平台 | 否 |
| rate_limited示例:请求过于频繁,请稍后用原幂等键重试 | 429 | 请求受限,本次请求未执行 | 商户、Key 维度的频率限制;换 Key 不能绕过商户总限额 | 等待 Retry-After 秒后用原幂等键重试 | 是 |
| internal_error示例:平台处理异常,请用原幂等键重试或按业务号查询 | 500 | 平台异常,请求可能已受理 | — | 用原幂等键重试或按商户订单号 / 退款号查询;不能换键重扣 | 是 |
| upstream_unavailable示例:依赖服务暂不可用,请求可能已受理 | 502 / 503 | 依赖异常,请求可能已受理 | 上游或平台依赖故障,平台无法确认本次请求是否已生效 | 同上 | 是 |
| temporarily_unavailable示例:服务暂时不可用,请稍后用原幂等键重试 | 503 | 暂时不可用,本次请求未受理 | 平台暂停了该通道的新交易;响应不说明内部原因 | 稍后用原幂等键重试;本次没有产生 Payment 或扣款;持续出现时联系平台运营 | 是 |
按接口查看
所有接口都可能返回 unauthenticated(401)、forbidden(403)、rate_limited(429)、internal_error(500)、upstream_unavailable(502 / 503);有参数或请求体的接口另含 invalid_request(400)。temporarily_unavailable(503)只由发起新交易的 createPayment、confirmPayment、createCheckoutSession 返回。下表只列各接口特有的错误码:
支付失败原因与 API 错误的区别
| 对比项 | API 错误(error.code) | 支付失败原因(failure.code) |
|---|---|---|
| 出现在哪里 | 4xx / 5xx 响应的 error 对象 | 2xx 响应或 Webhook 中 Payment 的 failure 对象 |
| 含义 | 本次请求被拒绝、冲突或结果未知 | 请求已受理,某一次支付尝试被明确拒绝(如发卡行拒绝) |
| 有没有 Payment | 通常没有新对象(409 时 related_resource_id 指向已有对象) | 有,status=failed,failure.attempt_id 指向失败的尝试 |
| 会不会有 Webhook | 不会 | 会,payment.failed(含尝试号与当前 version) |
| 怎么重试 | 修正请求;429 / 5xx 用原幂等键 | 付款人换卡,用新幂等键调用 confirm(有效期与尝试次数内) |
| 能否展示给付款人 | 一般不展示(集成或系统问题),可显示通用提示 | 可展示脱敏的 failure.message |
| 例子 | payment_in_progress、idempotency_conflict、insufficient_refund_funds | card_declined |
failed 不是终态:收到 payment.failed 后付款人仍可能重试成功,所以按 version 处理事件。3DS 认证成功但发卡行拒绝扣款时,支付为 failed + failure,不是 API 错误。
失败原因码
支付失败原因
由上游结果映射;客户端须容忍未知的 failure.code,按“支付失败、可提示付款人换卡”处理。
| failure.code | 类别 | 含义 | 付款人能否换卡重试 |
|---|---|---|---|
| card_declined | 发卡行拒绝 | 发卡行拒绝了交易(通用原因) | 能 |
| expired_card | 发卡行拒绝 | 卡已过期 | 能 |
| incorrect_card_data | 发卡行拒绝 | 卡号、有效期或 CVC 有误 | 能(核对后重新输入) |
| card_not_supported | 发卡行拒绝 | 该卡类型不被接受 | 能(换另一张卡) |
| authentication_failed | 认证失败 | 3DS 认证未通过 | 能 |
| authentication_expired | 付款人放弃 | 认证在有效期内未完成,且已确认未扣款 | 能 |
| risk_declined | 风控拒绝 | 平台或上游风控拒绝,不说明具体规则 | 视情况 |
| processing_error | 技术错误 | 上游处理失败,且已确认未扣款 | 能 |
| upstream_amount_mismatch | 技术错误 | 上游实际扣款的金额或币种与订单不一致,平台已把实扣金额原路退还持卡人;这笔退还不出现在退款接口中,也不影响你的余额与可退金额 | 视情况(订单仍在有效期与尝试次数内时可以) |
付款人关闭认证页时平台不推断结果,此时不会立即出现失败原因;只有上游确认未扣款后才出现 authentication_expired 等原因。
退款失败原因
只在退款 failed、rejected 时出现:
| failure.code | 含义 |
|---|---|
| refund_rejected | 退款审核驳回 |
| refund_failed | 上游明确未执行退款 |
处理示例
下面的函数把一次调用的结果归类为“下一步怎么做”。未知错误码走 default 分支,按 HTTP 状态处理。
const API_BASE = 'https://api.payhub.example';
// 返回"下一步怎么做";未知错误码按 HTTP 状态处理
export async function callPayHub(method, path, { body, idempotencyKey } = {}) {
let res;
try {
res = await fetch(`${API_BASE}${path}`, {
method,
headers: {
Authorization: `Bearer ${process.env.PAYHUB_API_KEY}`,
...(body ? { 'Content-Type': 'application/json' } : {}),
...(idempotencyKey ? { 'Idempotency-Key': idempotencyKey } : {})
},
body: body ? JSON.stringify(body) : undefined,
signal: AbortSignal.timeout(20_000)
});
} catch (cause) {
return { next: 'unknown', cause }; // 超时或网络错误:结果未知,同键重试或按业务号查询
}
const data = await res.json().catch(() => null); // 5xx 可能不是 JSON
const requestId = data?.request_id;
if (res.ok) return { next: 'check_status', httpStatus: res.status, data, requestId };
if (res.status === 429) {
const raw = res.headers.get('Retry-After');
const after = raw === null ? NaN : Number(raw);
return { next: 'retry_same_key', afterSeconds: Number.isFinite(after) ? after : 1, requestId };
}
if (res.status >= 500) {
// 503 temporarily_unavailable:本次请求未受理,稍后用原幂等键重试;其他 5xx 结果未知
if (data?.error?.code === 'temporarily_unavailable')
return { next: 'retry_same_key_later', requestId };
return { next: 'unknown', requestId };
}
const error = data?.error ?? {};
switch (error.code) {
case 'payment_in_progress':
case 'order_no_conflict':
case 'refund_no_conflict':
return { next: 'query_existing', id: error.related_resource_id, requestId };
case 'operation_in_progress':
return { next: 'retry_same_key', afterSeconds: 2, requestId };
case 'idempotency_conflict':
return { next: 'fix_integration', requestId }; // 同键不同参数:修复幂等键生成并告警
default:
return {
next: 'rejected',
httpStatus: res.status,
code: error.code,
field: error.field,
message: error.message,
requestId
};
}
}联系技术支持
- 先在商户后台按
request_id自查调用记录(方法、路径、耗时、HTTP 状态、脱敏错误摘要):API 调用记录(在新标签页打开商户后台)(需要所有者、管理员或开发者)。 - 联系时提供:
request_id、发生时间(带时区)、环境(测试 / 生产)、接口、商户订单号或平台对象 ID。 - 绝不要提供:API Key、Webhook 签名密钥、完整卡号、CVC、带令牌的认证或收银链接。