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

# Lỗi

Mọi response 4xx/5xx đều mang cùng envelope JSON:

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

- `code` — **HTTP status dạng số** (`400`, `401`, `404`, …). Hữu ích
  cho xử lý lớp HTTP chung, nhưng cho logic phân nhánh hãy switch
  trên `message` — `code` sẽ không phân biệt được giữa, ví dụ,
  `invalid_input` và `payment_method_not_supported` (cả hai đều 400).
- `message` — tên sentinel **lower-snake-case** (ví dụ `INVALID_INPUT`
  trở thành `"invalid_input"`). Ổn định qua các bản phát hành — switch
  trên cái này.
- `details` — được điền cho lỗi validation. Mảng các object
  `{ field, message }` để client có thể pin lỗi vào input. Bỏ qua
  trong các trường hợp khác.

> **Warning:**
>
> Body lỗi không bao gồm trace ID hay timestamp. Để báo cáo sự cố, hãy
> gửi cho support header `Date` của response, các header `X-RateLimit-*`,
> merchant ID, endpoint, và thời gian request xấp xỉ.

> **Note:**
>
> **Lỗi xác thực dùng cấu trúc (structure) khác.** Envelope ở trên là
> cái API trả về cho hầu hết các lỗi. Request bị reject trước khi được
> xử lý (`X-Signature` thiếu hoặc không hợp lệ, `X-Client-ID`
> không được nhận diện, hoặc `X-Timestamp` cũ trên lệnh gọi `/b2b/v1/*`)
> trả về dưới dạng `{ "error": "...", "message": "..." }`, trong đó
> `error` là slug thô (`unauthorized` / `bad_request` /
> `service_unavailable`) và `message` mang chi tiết. Không có `code`
> dạng số và không có `details`. Hãy phân nhánh trên HTTP status
> trước, rồi đọc `message`. Ví dụ (401):
>
> ```json
> { "error": "unauthorized", "message": "invalid signature" }
> ```

## HTTP status → mã điển hình

| HTTP | Các giá trị `code` điển hình | Ý nghĩa |
| --- | --- | --- |
| **400** | `INVALID_INPUT`, `MISSING_REQUIRED`, `INVALID_FORMAT`, `INVALID_LENGTH`, `INVALID_VALUE`, `PAYMENT_METHOD_NOT_SUPPORTED`, `AMOUNT_BELOW_MINIMUM` | Bad request — xem `details` |
| **401** | `INVALID_CREDENTIALS`, `INVALID_TOKEN`, `TOKEN_EXPIRED`, `INVALID_SIGNATURE` (B2B); `SESSION_EXPIRED` (chỉ dashboard) | Auth fail — key sai, timestamp hết hạn, chữ ký sai. Các mã đăng nhập dashboard không liên quan đến tích hợp B2B. |
| **402** | `INSUFFICIENT_CREDIT` | Số dư prepaid của merchant đã hết — top up trước khi retry |
| **403** | `FORBIDDEN`, `IP_BLOCKED` | Key hợp lệ nhưng thiếu scope/cấp phép IP cho lệnh gọi này |
| **404** | `NOT_FOUND`, `RECORD_NOT_FOUND` | Resource không tồn tại (hoặc không tồn tại cho merchant này) |
| **409** | `ALREADY_EXISTS` | Replay idempotency với body khác, hoặc state machine từ chối transition |
| **429** | `TOO_MANY_REQUESTS`, `TOO_MANY_ATTEMPTS` | Đạt rate limit; back off và retry |
| **500** | `INTERNAL_SERVER_ERROR`, `EXTERNAL_SERVICE_ERROR` | Lỗi của chúng tôi; an toàn để retry với backoff. |
| **503** | `SERVICE_UNAVAILABLE` | Một dependency downstream đang down. Retry với backoff |

## Tham chiếu mã đầy đủ

Các giá trị `code` bạn có thể thấy:

### Xác thực (401)
- `INVALID_CREDENTIALS` — combo API key bị reject
- `INVALID_TOKEN` — token không parse được hoặc bị tamper
- `TOKEN_EXPIRED` — token đã hết hạn
- `SESSION_EXPIRED` — session dashboard đã hết hạn
- `INVALID_SIGNATURE` — chữ ký HMAC không khớp trên lệnh gọi B2B / webhook

### Authorization (403)
- `FORBIDDEN` — đã xác thực, nhưng bạn không được phép thực hiện hành động này
- `IP_BLOCKED` — request từ địa chỉ IP này bị chặn

### Not found (404)
- `NOT_FOUND` — chung
- `RECORD_NOT_FOUND` — row thiếu cho ID đã cho

### Conflict (409)
- `ALREADY_EXISTS` — chung

### Validation (400)
- `INVALID_INPUT` — chung; xem `details`
- `MISSING_REQUIRED` — một trường bắt buộc bị thiếu
- `INVALID_FORMAT` — giá trị không khớp định dạng mong đợi (ví dụ UUID, URL, email)
- `INVALID_LENGTH` — giá trị quá ngắn hoặc quá dài
- `INVALID_VALUE` — giá trị nằm ngoài enum/range cho phép
- `PAYMENT_METHOD_NOT_SUPPORTED` — combo provider/asset chưa được enable cho merchant
- `AMOUNT_BELOW_MINIMUM` — amount của order dưới sàn theo từng network hoặc theo cấp env. `details.floor_usd` trong response mang sàn cấu hình (USD) để bạn có thể surface trực tiếp; text `message` cũng nêu rõ.
- `INSUFFICIENT_BALANCE` — ví của người mua không đủ pay asset để cover giao dịch chuyển.
- `INSUFFICIENT_GAS` — ví của người mua thiếu phí mạng (gas) bằng đồng native để broadcast giao dịch chuyển.

### Payment / billing (402)
- `INSUFFICIENT_CREDIT` — số dư prepaid của merchant không thể bù phí mạng (gas) và phí nền tảng. Top up qua dashboard, rồi retry

### Rate limiting (429)
- `TOO_MANY_REQUESTS` — vượt quá rate limit theo IP
- `TOO_MANY_ATTEMPTS` — các attempt fail lặp lại trên cùng resource (ví dụ OTP) chạm throttle

### Lỗi server (500)
- `EXTERNAL_SERVICE_ERROR` — một provider bên thứ ba bị lỗi
- `INTERNAL_SERVER_ERROR` — lỗi bất ngờ; liên hệ support kèm header `Date` của response và các header `X-RateLimit-*`

### Availability (503)
- `SERVICE_UNAVAILABLE` — dịch vụ tạm thời không khả dụng; retry với backoff

## Lỗi validation (400)

Khi vấn đề là body request bị lỗi định dạng, `details` là một mảng để
bạn có thể map lỗi về các trường:

```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" }
  ]
}
```

## Lỗi auth (401)

`INVALID_SIGNATURE` có ba nguyên nhân thường gặp:

- Sai secret key (kiểm tra lại env var)
- Drift timestamp > 5 phút (đồng bộ NTP)
- Sai chuỗi canonical (thường gặp nhất: quên separator `\n`, hoặc ký
  một body đã được parse rồi stringify lại khác với cái bạn đã gửi)

Xem [Xác thực](https://docs.infraio.xyz/vi/api-reference/authentication) cho thuật toán ký
chính xác.

## Rate limit (429)

| Surface | Giới hạn |
| --- | --- |
| Mọi route API (kể cả `/b2b/v1/*`) | Theo địa chỉ IP — **500 req/min**, bucket chung. Không theo merchant. |
| Public checkout (`/checkout/*`) | Sub-bucket chặt hơn — **20 req/min theo IP** |

`X-RateLimit-Limit` và `X-RateLimit-Remaining` được include trên mọi
response bị rate-limit, không chỉ response thành công. Coi chúng là
budget hiện tại cho IP của bạn.

Response bị rate-limit có include header `Retry-After` (số giây đến
khi cửa sổ reset) — hãy tôn trọng nó. Như một fallback, back off với
jitter — 1s base + exponential lên 30s.

> **Note:**
>
> Rate limit có thể thay đổi. Nếu bạn chạm chúng với traffic hợp lệ
> (ví dụ đối soát một khoảng lịch sử lớn), hãy liên hệ support.

## Thiếu credit (402)

`INSUFFICIENT_CREDIT` (HTTP **402 Payment Required**) nghĩa là số dư
prepaid của bạn không thể bù phí mạng (gas) và phí nền tảng cho
thao tác bạn cố thực hiện, thường là settle một thanh toán crypto
hoặc một hành động on-chain được tài trợ phí mạng. Top up từ
merchant dashboard (**Billing → Add credit**), rồi retry thao tác.
Các thao tác đang chạy sẽ tiếp tục tự động khi số dư được top up.

## Lỗi server (5xx)

500 nghĩa là chúng tôi không thể xử lý request. Retry với backoff —
`idempotency_key` của bạn đảm bảo bạn không charge gấp đôi nếu request
ban đầu đã thành công một phần.

Nếu retry không recover trong một phút, surface một "thanh toán tạm
thời không khả dụng" chung cho người mua và liên hệ support với
endpoint fail, merchant ID của bạn, header `Date` của response và
các giá trị `X-RateLimit-*`, và thời gian request xấp xỉ — đó là đủ
cho chúng tôi tìm ra request.

## Lỗi delivery webhook

Delivery webhook là một kênh failure riêng — chúng không surface dưới
dạng lỗi API vì server của bạn không phải là bên đang gọi. Khi một
delivery trả về non-2xx (hoặc timeout), nó được retry với
exponential backoff tại 0s, 1min, 5min, 15min, 1h, 6h (tổng sáu lần,
cùng schedule như [Webhooks → Tổng quan](https://docs.infraio.xyz/vi/webhooks/overview)).
Lịch sử đầy đủ theo từng delivery hiển thị dưới **Developers →
Webhooks → [endpoint] → Delivery log** trong dashboard. Sau lần thứ
sáu, delivery được đánh dấu "Failed" trong dashboard, và bạn có thể
replay thủ công khi server của bạn khỏe trở lại.
