# 오류

> Ikiform이 반환하는 모든 오류 코드와 HTTP 상태, 원인, 해결 방법.

폼 제출과 REST API는 같은 오류 형식을 사용합니다. JSON 요청은 해당 HTTP 상태와 함께 이 형식을 받고, 브라우저 폼 POST는 같은 메시지와 상태가 담긴 호스팅 오류 페이지를 받습니다.

## 오류 형식

`Error`:

```json
{
  "ok": false,
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Please slow down."
  },
  "request_id": "req_01k7v2c9t3e8m5q4x6z0a1b2c3"
}
```

- `error.code`는 바뀌지 않으므로 분기 처리에 사용하세요. `error.message`는 사람이 읽는 평이한 영어 문장이며 바뀔 수 있습니다.
- `error.fields`는 검증 오류에 포함되며, 필드 ID를 키로 메시지 목록을 담습니다. 본문 전체에 대한 오류는 `_form` 키를 사용합니다.
- `error.challenge_url`은 `challenge_required`에 포함됩니다.
- `request_id`는 모든 응답에 붙는 `X-Request-Id` 헤더와 같습니다.
- 레이트 리밋 응답에는 초 단위의 `Retry-After`가 붙습니다. 두 헤더 모두 브라우저 코드에서 읽을 수 있습니다.

## 코드

| 상태 | 코드 | 발생 위치 |
| --- | --- | --- |
| 400 | [`invalid_body`](#invalid-body) | 제출 |
| 400 | [`invalid_request`](#invalid-request) | API |
| 401 | [`unauthorized`](#unauthorized) | API |
| 402 | [`plan_required`](#plan-required) | 제출 |
| 403 | [`forbidden`](#forbidden) | 둘 다 |
| 403 | [`origin_not_allowed`](#origin-not-allowed) | 제출 |
| 403 | [`form_paused`](#form-paused) | 제출 |
| 403 | [`captcha_failed`](#captcha-failed) | 제출 |
| 403 | [`challenge_required`](#challenge-required) | 제출 |
| 404 | [`not_found`](#not-found) | 둘 다 |
| 405 | [`method_not_allowed`](#method-not-allowed) | API |
| 409 | [`idempotency_in_progress`](#idempotency-in-progress) | 제출 |
| 413 | [`payload_too_large`](#payload-too-large) | 제출 |
| 415 | [`unsupported_media_type`](#unsupported-media-type) | 제출 |
| 415 | [`unsupported_charset`](#unsupported-charset) | 제출 |
| 422 | [`validation_failed`](#validation-failed) | 제출 |
| 422 | [`files_not_supported`](#files-not-supported) | 제출 |
| 422 | [`too_deep`](#too-deep) | 제출 |
| 422 | [`idempotency_mismatch`](#idempotency-mismatch) | 제출 |
| 429 | [`rate_limited`](#rate-limited) | 둘 다 |
| 429 | [`quota_exceeded`](#quota-exceeded) | 제출 |
| 500 | [`server_error`](#server-error) | 둘 다 |

### 400 invalid_body

|   |   |
| --- | --- |
| 메시지 | `The request body could not be read.` |
| 원인 | 형식이 잘못된 JSON이나 multipart, 객체 대신 JSON 배열이나 문자열, 또는 255자를 넘는 멱등성 키. |
| 해결 | JSON 객체 하나를 보내거나, 브라우저나 `FormData`가 본문을 만들게 하세요. |

### 400 invalid_request

|   |   |
| --- | --- |
| 메시지 | `The request is invalid.` |
| 원인 | `limit`, `status`, `form_id`, `cursor` 중 하나가 잘못되었습니다. 메시지에 해당 파라미터가 표시됩니다. |
| 해결 | `limit`은 1~100, `status`는 `received` 또는 `spam`, 실제 `frm_` ID를 사용하고, 커서는 반환된 그대로 사용하세요. |

### 401 unauthorized

|   |   |
| --- | --- |
| 메시지 | `A valid API key is required.` |
| 원인 | `Authorization` 헤더가 없거나, Bearer 토큰이 아니거나, 알 수 없거나 폐기되었거나 만료된 키입니다. |
| 해결 | 유효한 시크릿 키로 `Authorization: Bearer ik_sk_...`를 보내세요. |

### 402 plan_required

|   |   |
| --- | --- |
| 메시지 | `This form isn't accepting submissions right now.` |
| 원인 | 워크스페이스에 활성 플랜이 없습니다. 결제 기능이 켜진 뒤에만 발생할 수 있습니다. |
| 해결 | 대시보드의 **Settings**에서 플랜을 고르세요. |

### 403 forbidden

|   |   |
| --- | --- |
| 메시지 | `You do not have access to this resource.` |
| 원인 | API 키의 형식이 잘못되었거나, 다른 워크스페이스의 키이거나, REST API에 퍼블리셔블 키를 썼거나, 필요한 스코프가 없습니다. |
| 해결 | 이 워크스페이스의 시크릿 키를 메시지에 적힌 스코프와 함께 사용하세요. |

### 403 origin_not_allowed

|   |   |
| --- | --- |
| 메시지 | `This form does not accept submissions from this site.` |
| 원인 | 폼의 허용 도메인 밖의 사이트에서 POST가 왔고 폼이 불일치를 거부하도록 설정되어 있거나, 키에 등록되지 않은 도메인에서 퍼블리셔블 키를 사용했습니다. |
| 해결 | 사이트를 **Allowed domains**나 키의 도메인에 추가하세요. 서버에서 POST할 때는 시크릿 키를 사용하세요. |

### 403 form_paused

|   |   |
| --- | --- |
| 메시지 | `This form is not accepting submissions right now.` |
| 원인 | 대시보드에서 폼이 일시 중지되어 있습니다. |
| 해결 | 폼을 다시 시작하세요. |

### 403 captcha_failed

|   |   |
| --- | --- |
| 메시지 | `The captcha check failed. Please try again.` |
| 원인 | Turnstile 토큰이 없거나, 만료되었거나, 이미 사용되었거나, 검증되지 않았습니다. |
| 해결 | 제출할 때마다 새 토큰을 받으세요. 직접 발급한 키를 쓴다면 사이트 키와 시크릿이 짝이 맞는지 확인하세요. |

### 403 challenge_required

|   |   |
| --- | --- |
| 메시지 | `Complete the human check, then send the form again.` |
| 원인 | 폼이 Ikiform 사람 확인을 사용하는데 토큰 없이 JSON POST가 도착했습니다. |
| 해결 | SDK의 `challenge(formId)`를 호출해 토큰을 전달하거나 `error.challenge_url`을 사용하세요. 서버 POST는 대신 시크릿 키를 보낼 수 있습니다. |

### 404 not_found

|   |   |
| --- | --- |
| 메시지 | `Not found.` |
| 원인 | 폼이 존재하지 않거나, 삭제 또는 비활성화되었거나, ID나 경로가 잘못되었습니다. `/f/<formId>`로 `GET` 요청을 보내도 반환됩니다. |
| 해결 | 폼 개요에서 엔드포인트를 복사하고, 탭에서 여는 대신 POST하세요. |

### 405 method_not_allowed

|   |   |
| --- | --- |
| 메시지 | `This method is not allowed here.` |
| 원인 | 서버 간 통신 전용인 REST API에 대한 브라우저 프리플라이트입니다. |
| 해결 | 백엔드에서 `/api/v1`을 호출하세요. |

### 409 idempotency_in_progress

|   |   |
| --- | --- |
| 메시지 | `A request with this idempotency key is still being processed.` |
| 원인 | 이 멱등성 키를 쓴 요청이 아직 처리 중입니다. |
| 해결 | 잠시 기다린 뒤 같은 키로 다시 시도하면 첫 응답을 받습니다. |

### 413 payload_too_large

|   |   |
| --- | --- |
| 메시지 | `The submission is too large.` |
| 원인 | 본문, 필드 이름, 값, 필드 수 중 하나가 한도를 넘었습니다. 메시지에 어느 것인지 표시됩니다. |
| 해결 | [한도](https://ikiform.com/ko/docs/limits.md#submissions)를 참고하세요. 긴 텍스트는 여러 필드로 나누거나, 파일은 다른 곳에 저장하고 링크를 보내세요. |

### 415 unsupported_media_type

|   |   |
| --- | --- |
| 메시지 | `Send the form as urlencoded, multipart or JSON.` |
| 원인 | `Content-Type`이 urlencoded, multipart, JSON이 아니거나 없습니다. |
| 해결 | JSON을 POST할 때는 `Content-Type: application/json`을 설정하세요. |

### 415 unsupported_charset

|   |   |
| --- | --- |
| 메시지 | `Only UTF-8 is supported.` |
| 원인 | 본문이 UTF-8이 아니거나 다른 문자 집합을 선언했습니다. |
| 해결 | UTF-8로 인코딩하세요. 오래된 HTML 폼에는 `accept-charset="utf-8"`을 추가하세요. |

### 422 validation_failed

|   |   |
| --- | --- |
| 메시지 | `Some fields are invalid.` |
| 원인 | 필드가 검증에 실패했습니다. `error.fields`가 필드 ID와 메시지를 연결합니다. |
| 해결 | 메시지를 필드 옆에 보여 주세요. [필드](https://ikiform.com/ko/docs/fields.md#errors)를 참고하세요. |

### 422 files_not_supported

|   |   |
| --- | --- |
| 메시지 | `File uploads are not supported yet.` |
| 원인 | POST에 파일이 포함되어 있습니다. 파일 업로드는 아직 지원하지 않습니다. |
| 해결 | 파일 입력을 제거하거나, 파일을 다른 곳에 업로드하고 링크를 제출하세요. |

### 422 too_deep

|   |   |
| --- | --- |
| 메시지 | `The JSON body is nested too deeply.` |
| 원인 | JSON 본문이 최상위 객체를 포함해 3단계보다 깊게 중첩되어 있습니다. |
| 해결 | `{ "address_city": "London" }`처럼 데이터를 평평하게 만드세요. |

### 422 idempotency_mismatch

|   |   |
| --- | --- |
| 메시지 | `This idempotency key was already used with a different payload.` |
| 원인 | 이 멱등성 키가 최근 24시간 안에 이 폼에서 다른 필드와 함께 이미 사용되었습니다. |
| 해결 | 새 데이터에는 새 키를 사용하세요. 같은 키는 완전히 같은 재시도에만 쓰세요. |

### 429 rate_limited

|   |   |
| --- | --- |
| 메시지 | `Too many requests. Please slow down.` |
| 원인 | 레이트 리밋을 넘었습니다. `Retry-After`가 기다려야 할 초를 알려 줍니다. |
| 해결 | 기다린 뒤 다시 시도하세요. 실제 방문자가 한도에 닿는다면 폼의 [레이트 리밋](https://ikiform.com/ko/docs/rate-limits.md)을 바꾸세요. |

### 429 quota_exceeded

|   |   |
| --- | --- |
| 메시지 | `This form isn't accepting submissions right now.` |
| 원인 | 워크스페이스가 이번 달에 월간 제출 한도의 2배를 받았습니다. |
| 해결 | 업그레이드하거나 다음 달을 기다리세요. [월간 사용량](https://ikiform.com/ko/docs/limits.md#monthly-usage)을 참고하세요. |

### 500 server_error

|   |   |
| --- | --- |
| 메시지 | `Something went wrong on our side.` |
| 원인 | Ikiform 쪽에서 문제가 발생했습니다. |
| 해결 | 백오프와 멱등성 키를 사용해 다시 시도하세요. 계속 발생하면 `request_id`를 알려 주세요. |

## 오류가 아닌 경우

- 스팸은 실제 제출과 같은 `201`이나 리디렉션을 받습니다. 대시보드의 스팸 필터에서 확인하세요.
- 허용되지 않은 `_redirect`는 무시되며, 브라우저는 호스팅 감사 페이지로 이동합니다.
- 올바른 이메일 주소 하나가 아닌 `_replyto`는 무시됩니다.

## 재시도

- `429`는 `Retry-After` 후에, `500`, `409`, 네트워크 오류는 백오프로 재시도하세요. [멱등성 키](https://ikiform.com/ko/docs/frameworks.md#idempotency)를 보내면 재시도해도 중복이 생기지 않습니다.
- 다른 `4xx` 오류는 그대로 재시도하지 마세요. 먼저 요청을 고치세요.
