<!-- Source: https://docs.infraio.xyz/zh-CN/api-reference/errors -->
<!-- Last updated: 2026-10-04 -->

# 错误

每个 4xx/5xx 响应都携带相同的 JSON 信封:

```json
{
  "code": 400,
  "message": "invalid_input",
  "details": [
    { "field": "items[0].unit_price", "message": "must be a positive decimal string" }
  ]
}
```

- `code` — **数字形式的 HTTP 状态码**(`400`、`401`、`404`、……)。适合做通用的 HTTP 层处理,但分支逻辑请基于 `message` — `code` 无法区分例如 `invalid_input` 与 `payment_method_not_supported`(两者都是 400)。
- `message` — **lower-snake-case** 的 sentinel 名的小写蛇形形式(例如 `INVALID_INPUT` 对应 `"invalid_input"`)。跨版本稳定 — 请基于此分支。
- `details` — 仅在校验错误时出现。`{ field, message }` 对象数组,便于前端把错误对应到输入框。其他情况省略。

> **Warning:**
>
> 错误 body 不包含 trace ID 或时间戳。反馈问题时,请向支持团队提供响应的 `Date` 标头、`X-RateLimit-*` 标头、你的商户 ID、端点,以及请求的大致时间。

> **Note:**
>
> **鉴权失败使用不同的形态。** 上面的信封是 API 对大多数错误的返回形式。在被处理之前就被拒绝的请求 —— `/b2b/v1/*` 调用上缺失 / 非法的 `X-Signature`、未知的 `X-Client-ID`,或过期的 `X-Timestamp` —— 会以 `{ "error": "...", "message": "..." }` 的形态返回,其中 `error` 是一个粗粒度 slug(`unauthorized` / `bad_request` / `service_unavailable`),`message` 携带具体信息。没有数值 `code`,也没有 `details`。请先按 HTTP 状态码分支,再读 `message`。示例(401):
>
> ```json
> { "error": "unauthorized", "message": "invalid signature" }
> ```

## HTTP 状态码 → 典型 code

| HTTP | 典型 `code` 值 | 含义 |
| --- | --- | --- |
| **400** | `INVALID_INPUT`、`MISSING_REQUIRED`、`INVALID_FORMAT`、`INVALID_LENGTH`、`INVALID_VALUE`、`PAYMENT_METHOD_NOT_SUPPORTED`、`AMOUNT_BELOW_MINIMUM` | 请求错误 — 看 `details` |
| **401** | `INVALID_CREDENTIALS`、`INVALID_TOKEN`、`TOKEN_EXPIRED`、`INVALID_SIGNATURE`(B2B);`SESSION_EXPIRED`(仅限仪表板) | 鉴权失败 — 密钥错误、时间戳过期、签名错误。仪表板登录相关的 code 与 B2B 集成无关。 |
| **402** | `INSUFFICIENT_CREDIT` | 商户预付余额已用尽 — 充值后重试 |
| **403** | `FORBIDDEN`、`IP_BLOCKED` | 密钥有效但缺少此调用所需的 scope 或 IP 许可 |
| **404** | `NOT_FOUND`、`RECORD_NOT_FOUND` | 资源不存在(或对本商户不存在) |
| **409** | `ALREADY_EXISTS` | 用不同 body 重放幂等键,或状态机拒绝转换 |
| **429** | `TOO_MANY_REQUESTS`、`TOO_MANY_ATTEMPTS` | 触发限流;退避后重试 |
| **500** | `INTERNAL_SERVER_ERROR`、`EXTERNAL_SERVICE_ERROR` | 我们的问题;可安全退避重试。 |
| **503** | `SERVICE_UNAVAILABLE` | 下游依赖宕机。退避后重试 |

## 完整 code 参考

你可能看到的 `code` 值:

### 鉴权 (401)
- `INVALID_CREDENTIALS` — API 密钥组合被拒
- `INVALID_TOKEN` — token 无法解析或被篡改
- `TOKEN_EXPIRED` — token 已过期
- `SESSION_EXPIRED` — 仪表板会话过期
- `INVALID_SIGNATURE` — B2B / Webhook 调用上的 HMAC 签名不一致

### 授权 (403)
- `FORBIDDEN` — 已认证,但你无权执行该操作
- `IP_BLOCKED` — 来自该 IP 地址的请求被阻止

### 未找到 (404)
- `NOT_FOUND` — 通用
- `RECORD_NOT_FOUND` — 给定 ID 的行缺失

### 冲突 (409)
- `ALREADY_EXISTS` — 通用

### 校验 (400)
- `INVALID_INPUT` — 通用;请查看 `details`
- `MISSING_REQUIRED` — 缺失必填字段
- `INVALID_FORMAT` — 值不匹配预期格式(如 UUID、URL、email)
- `INVALID_LENGTH` — 值过短或过长
- `INVALID_VALUE` — 值超出允许的枚举/范围
- `PAYMENT_METHOD_NOT_SUPPORTED` — 该商户未启用此 provider/asset 组合
- `AMOUNT_BELOW_MINIMUM` — 订单金额低于每网络或环境级别的下限。响应中的 `details.floor_usd` 携带配置的下限(美元),可直接展示;`message` 文本中也会说明。
- `INSUFFICIENT_BALANCE` — 买家钱包中该支付资产的余额不足以完成转账。
- `INSUFFICIENT_GAS` — 买家钱包缺少用于支付网络手续费(gas)的原生代币,无法广播转账。

### 支付 / 计费 (402)
- `INSUFFICIENT_CREDIT` — 商户预付余额不足以覆盖该操作的网络手续费(gas)/ 平台手续费。从仪表板充值后重试

### 限流 (429)
- `TOO_MANY_REQUESTS` — 超出 IP 限流
- `TOO_MANY_ATTEMPTS` — 同一资源(如 OTP)上重复失败触发节流

### 服务端错误 (500)
- `EXTERNAL_SERVICE_ERROR` — 第三方 provider 失败
- `INTERNAL_SERVER_ERROR` — 意外错误;请联系支持团队,并附上响应的 `Date` 标头与 `X-RateLimit-*` 标头

### 可用性 (503)
- `SERVICE_UNAVAILABLE` — 服务暂时不可用;退避后重试

## 校验错误 (400)

当问题是请求 body 格式错误时,`details` 是数组,这样你就能把错误映射回字段:

```json
{
  "code": 400,
  "message": "invalid_input",
  "details": [
    { "field": "items[0].unit_price", "message": "must be a positive decimal string" },
    { "field": "success_url", "message": "must be a valid https URL" }
  ]
}
```

## 鉴权错误 (401)

`INVALID_SIGNATURE` 常见有三种原因:

- secret key 错(再检查环境变量)
- 时间戳偏移 > 5 分钟(同步 NTP)
- 规范化字符串错(最常见:忘了 `\n` 分隔符,或对解析后又重新序列化的
  body 签名 — 与你实际发送的不一致)

精确签名算法见 [身份验证](https://docs.infraio.xyz/zh-CN/api-reference/authentication)。

## 限流 (429)

| 表面 | 限制 |
| --- | --- |
| 所有 API 路由(含 `/b2b/v1/*`) | 按 IP 地址限流 — **500 req/min**,共享桶。非按商户的配额。 |
| 公共结账 (`/checkout/*`) | 更严格的子桶 — **每 IP 20 req/min** |

`X-RateLimit-Limit` 与 `X-RateLimit-Remaining` 会包含在每个被限流的响应中,不仅是成功响应。请将其视为你 IP 的实时预算。

被限流的响应会包含 `Retry-After` 标头(距离限流窗口重置的秒数)——请遵循它。作为兜底,请带抖动退避 — 1s 基数 + 指数到 30s。

> **Note:**
>
> 限流可能调整。如果你的正常流量触达了限流(例如对账大段历史数据),请
> 联系支持团队。

## 余额不足 (402)

`INSUFFICIENT_CREDIT`(HTTP **402 Payment Required**)表示你的预付余额
无法覆盖所执行操作的网络手续费(gas)和平台手续费,典型场景是结算加密
支付,或执行网络手续费由平台代付的链上动作。请从商户仪表板充值
(**Billing → Add credit**),然后重试该操作。已在进行中的操作会在余额
充值后自动继续。

## 服务端错误 (5xx)

500 表示我们无法处理该请求。带退避重试 — 你的 `idempotency_key`
确保即使原请求已部分成功,也不会重复扣款。

如果一分钟内重试无法恢复,请向买家展示通用的"支付暂时不可用",
并联系支持团队,附上失败的端点、你的商户 ID、响应的 `Date` 标头与
`X-RateLimit-*` 的值,以及请求的大致时间 — 这些足以让我们找到该请求。

## Webhook 投递错误

Webhook 投递是一条独立的失败通道 — 它们不以 API 错误形式出现,因为
不是你的服务器在调用。当一次投递返回非 2xx(或超时),它会按指数
退避在 0s、1min、5min、15min、1h、6h 重试(总共六次 —
与 [Webhooks → 概览](https://docs.infraio.xyz/zh-CN/webhooks/overview) 的时间表一致)。完整
的每次投递历史显示在仪表板的 **Developers → Webhooks → [endpoint] →
Delivery log**。第六次尝试失败后,该投递在仪表板中被标记为 "Failed",
你可以在服务器恢复后手动重放。
