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

# 오류

모든 4xx/5xx 응답은 동일한 JSON envelope를 가집니다.

```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` — **소문자 스네이크 케이스**로 표기한 센티넬 이름
  (예: `INVALID_INPUT`은 `"invalid_input"`이 됩니다). 릴리스 간 안정적
  입니다 — 이것으로 분기하세요.
- `details` — 검증 오류 시 채워집니다. 클라이언트가 오류를 입력에 매핑할
  수 있도록 `{ field, message }` 객체 배열입니다. 그 외에는 생략됩니다.

> **Warning:**
>
> 오류 본문에는 trace ID나 타임스탬프가 포함되지 않습니다. 문제를 신고할 때는
> 응답 `Date` 헤더, `X-RateLimit-*` 헤더, 가맹점 ID, 엔드포인트, 대략적인 요청
> 시각을 지원팀에 보내세요.

> **Note:**
>
> **인증 실패는 다른 형식을 사용합니다.** 위의 envelope는 대부분의 오류에서
> API가 반환하는 형식입니다. 처리되기 전에 거부된 요청(`/b2b/v1/*` 호출에서
> 누락/잘못된 `X-Signature`, 알 수 없는 `X-Client-ID`, 또는 오래된
> `X-Timestamp`)은 `{ "error": "...", "message": "..." }` 형태로 돌아옵니다.
> 여기서 `error`는 거친 슬러그(`unauthorized` / `bad_request` /
> `service_unavailable`)이고 `message`는 세부 사항을 전달합니다. 숫자 `code`도,
> `details`도 없습니다. 먼저 HTTP 상태로 분기한 다음 `message`를 읽으세요.
> 예시(401):
>
> ```json
> { "error": "unauthorized", "message": "invalid signature" }
> ```

## HTTP 상태 → 일반적인 코드

| 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` (대시보드만) | 인증 실패 — 잘못된 키, 만료된 타임스탬프, 잘못된 서명. 대시보드 로그인 코드는 B2B 통합과 무관합니다. |
| **402** | `INSUFFICIENT_CREDIT` | 가맹점 선불 잔액 소진 — 재시도 전 충전 필요 |
| **403** | `FORBIDDEN`, `IP_BLOCKED` | 키는 유효하지만 이 호출에 대한 스코프/IP 허용 권한 없음 |
| **404** | `NOT_FOUND`, `RECORD_NOT_FOUND` | 리소스가 존재하지 않음(또는 가맹점에 대해 존재하지 않음) |
| **409** | `ALREADY_EXISTS` | 다른 본문으로 멱등성 재시도, 또는 상태 머신이 전이를 거부함 |
| **429** | `TOO_MANY_REQUESTS`, `TOO_MANY_ATTEMPTS` | 레이트 리밋 도달. 백오프 후 재시도 |
| **500** | `INTERNAL_SERVER_ERROR`, `EXTERNAL_SERVICE_ERROR` | 저희 측 문제. 백오프로 재시도 안전. |
| **503** | `SERVICE_UNAVAILABLE` | 다운스트림 종속성 다운. 백오프 후 재시도 |

## 전체 코드 레퍼런스

볼 수 있는 `code` 값:

### 인증 (401)
- `INVALID_CREDENTIALS` — API 키 조합 거부됨
- `INVALID_TOKEN` — 토큰이 파싱 불가하거나 변조됨
- `TOKEN_EXPIRED` — 토큰 만료
- `SESSION_EXPIRED` — 대시보드 세션 만료
- `INVALID_SIGNATURE` — B2B / 웹훅 호출의 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` — 값이 허용된 enum/범위를 벗어남
- `PAYMENT_METHOD_NOT_SUPPORTED` — 제공자/자산 조합이 가맹점에 활성화되지 않음
- `AMOUNT_BELOW_MINIMUM` — 주문 금액이 네트워크별 또는 env 레벨 하한선
  미만. 응답의 `details.floor_usd`에 구성된 하한선(USD)이 담겨 있으므로
  바로 노출할 수 있습니다 — `message` 텍스트에도 동일한 내용이 표시됩니다.
- `INSUFFICIENT_BALANCE` — 구매자 지갑이 전송 결제 자산을 충분히 보유하고
  있지 않음.
- `INSUFFICIENT_GAS` — 구매자 지갑에 전송을 브로드캐스트할 네이티브
  토큰(네트워크 수수료용)이 부족함.

### 결제 / 청구 (402)
- `INSUFFICIENT_CREDIT` — 가맹점 선불 잔액으로 네트워크 수수료(가스)와 플랫폼 수수료를
  커버할 수 없음. 대시보드에서 충전 후 재시도

### 레이트 리밋 (429)
- `TOO_MANY_REQUESTS` — IP 레이트 리밋 초과
- `TOO_MANY_ATTEMPTS` — 동일 리소스에 반복된 실패 시도(예: OTP)가
  스로틀 발동

### 서버 오류 (500)
- `EXTERNAL_SERVICE_ERROR` — 서드파티 제공자 실패
- `INTERNAL_SERVER_ERROR` — 예상치 못한 오류. 응답 `Date` 헤더와
  `X-RateLimit-*` 헤더를 지원팀에 전달하여 문의

### 가용성 (503)
- `SERVICE_UNAVAILABLE` — 서비스를 일시적으로 사용할 수 없음. 백오프 후 재시도

## 검증 오류 (400)

문제가 잘못된 요청 본문인 경우 `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`의 일반적인 원인은 세 가지입니다.

- 잘못된 시크릿 키(env 변수 재확인)
- 타임스탬프 드리프트 > 5분(NTP 동기화)
- 잘못된 정규 문자열(가장 흔한 경우: `\n` 구분자 누락, 또는 보낸 것과
  다른 파싱-후-재문자열화된 본문 서명)

정확한 서명 알고리즘은 [인증](https://docs.infraio.xyz/ko/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**)는 선불 잔액이
수행하려는 작업에 대한 네트워크 수수료(가스)와 플랫폼 수수료를 커버할 수 없음을
의미합니다. 일반적으로 암호화폐 결제 정산 또는 네트워크 수수료를 후원하는 온체인
작업에서 발생합니다. 가맹점 대시보드(**Billing → Add credit**)에서 충전한 후
작업을 재시도하세요. 이미 진행 중인 작업은 잔액이 충전되면 자동으로 이어집니다.

## 서버 오류 (5xx)

500은 요청을 처리할 수 없었음을 의미합니다. 백오프로 재시도하세요 —
`idempotency_key`는 원래 요청이 부분적으로 성공했더라도 이중 청구를
방지합니다.

재시도가 1분 이내에 회복되지 않으면 구매자에게 일반적인 "결제 일시
사용 불가" 메시지를 노출하고 실패한 엔드포인트, 가맹점 ID, 응답 `Date`
헤더와 `X-RateLimit-*` 값, 대략적인 요청 시간을 지원팀에 문의하세요 —
이는 저희가 해당 요청을 찾기에 충분합니다.

## 웹훅 전달 오류

웹훅 전달은 별도의 실패 채널입니다 — 호출하는 것이 가맹점 서버가 아니므로
API 오류로 노출되지 않습니다. 전달이 non-2xx를 반환하거나 타임아웃되면,
0s, 1min, 5min, 15min, 1h, 6h(총 6회 시도 — [웹훅 → 개요](https://docs.infraio.xyz/ko/webhooks/overview)와
동일한 스케줄)에서 지수 백오프로 재시도됩니다. 전달별
전체 이력은 대시보드의 **Developers → Webhooks → [endpoint] → Delivery log**
패널에 표시됩니다. 여섯 번째 시도가 실패하면 대시보드에서 해당 전달이
"Failed"로 표시되며, 서버가 정상화된 후 수동으로 재생할 수 있습니다.
