오류
오류 응답 형식, HTTP 상태 코드별 의미와 재시도 여부
2xx가 아닌 모든 응답은 같은 JSON 형식을 사용합니다.
{
"defined": true,
"code": "CONFLICT",
"status": 409,
"message": "Conflict",
"data": {}
}| 필드 | 설명 |
|---|---|
defined | 스펙에 정의된 오류면 true, 예상하지 못한 오류면 false입니다. false여도 형식은 같습니다. |
code | 오류 코드 문자열(아래 표). 분기 처리에는 message가 아니라 code를 사용하세요. |
status | HTTP 상태 코드와 같은 값입니다. |
message | 사람이 읽기 위한 설명입니다. 문구는 바뀔 수 있습니다. |
data | 추가 정보(선택). 402(data.code)와 429(data.limit, data.policy, data.retryAfterMs)에 정의되어 있습니다. |
POST /v1/verify의 검증 실패(코드 불일치, 만료 등)는 오류가 아니라 200 + verified: false입니다. 검증 결과를 참고하세요.
상태 코드
| 상태 | code | 주요 원인 | 재시도 |
|---|---|---|---|
| 400 | BAD_REQUEST | 요청 형식 오류, 멱등 키 누락·공백, 헤더와 본문의 멱등 키 불일치, 알 수 없는 템플릿·템플릿 변수, expiresInSec/maxAttempts/cost 범위 초과, code가 6자리 숫자가 아님, 잘못된 limit·cursor·기간 | 아니요. 요청을 고치세요. |
| 401 | UNAUTHORIZED | Authorization: Bearer 키가 없음, 형식 오류, 비활성(폐기·만료) 또는 인식할 수 없는 키 | 아니요. 키를 확인하세요. |
| 402 | PAYMENT_REQUIRED | 크레딧 부족. data.code가 INSUFFICIENT_CREDIT 또는 OVERDRAFT_LIMIT_EXCEEDED (POST /v1/issue만 해당) | 충전 후 재시도 |
| 403 | FORBIDDEN | 키에 필요한 scope가 없음, pk_ 키로 서버 전용 API 호출, pk_ 키 요청에 Origin이 없거나 허용 목록과 불일치 | 아니요. 키 설정을 확인하세요. |
| 404 | NOT_FOUND | 해당 앱에 없는 issueId(다른 앱의 것 포함) 또는 templateId | 아니요 |
| 409 | CONFLICT | 같은 멱등 키를 다른 요청 본문으로 재사용, 또는 그 키의 issue가 이미 새 발급으로 교체됨 | 아니요 |
| 429 | TOO_MANY_REQUESTS | POST /v1/issue·POST /v1/verify가 키당·IP당·휴대폰 번호당 호출 제한을 넘음(호출 제한 참고). data.limit은 perKey, perIp, perPhone(verify는 perKey만) | 예. Retry-After·data.retryAfterMs 뒤에 같은 멱등 키로 |
| 500 | INTERNAL_SERVER_ERROR | 예상하지 못한 서버 오류 | 같은 멱등 키로 백오프 재시도 |
| 503 | SERVICE_UNAVAILABLE | 인증 또는 내부 의존성 일시 장애, pk_ 키에서 호출 제한기를 쓸 수 없음(요청을 거절하는 fail-closed, Retry-After 포함), 같은 멱등 키의 이전 요청이 아직 처리 중 | 같은 멱등 키로 백오프 재시도 |
테스트 모드에는 400 BAD_REQUEST 원인이 두 가지 더 있습니다. data.code가 TEST_NUMBER_REQUIRED이면 테스트 키로 실제 번호를 보낸 것이고, TEST_NUMBER_IN_LIVE_MODE이면 라이브 키로 매직 테스트 번호를 보낸 것입니다. 테스트 모드를 참고하세요.
발급에는 400 BAD_REQUEST 원인이 두 가지 더 있으며 data.code로 구분합니다. CHANNEL_NOT_SUPPORTED는 channel: "sms"를 요청한 경우(alimtalk만 받습니다)이고, TEMPLATE_NOT_AVAILABLE은 템플릿에 승인된 알림톡 템플릿이 아직 없는 경우입니다(채널과 SMS 대체 발송 참고).
호출 제한: 429와 Retry-After
호출 제한을 넘은 요청은 아무것도 발송하기 전에 429 TOO_MANY_REQUESTS로 거절되며, 제한 횟수(또는 검증 시도 횟수)에 포함되지 않습니다.
{
"defined": true,
"code": "TOO_MANY_REQUESTS",
"status": 429,
"message": "Too Many Requests",
"data": { "limit": "perPhone", "policy": "key", "retryAfterMs": 1500 }
}| 필드 | 의미 |
|---|---|
data.limit | 요청을 거절한 규칙: perKey, perIp, perPhone(perIp와 perPhone은 발급에만 적용됩니다). |
data.policy | API 키에 설정한 규칙이면 key, pk_ 기본값이나 플랫폼 상한이면 platform입니다. |
data.retryAfterMs | 재시도가 허용될 때까지 남은 시간(밀리초). |
Retry-After | 같은 대기 시간을 초 단위(올림, 최소 1)로 담은 헤더입니다. 허용된 브라우저 출처에서도 읽을 수 있어 pk_ 브라우저 클라이언트가 따를 수 있습니다. |
그만큼 기다린 뒤 issue는 같은 멱등 키로 다시 시도하세요.
503 SERVICE_UNAVAILABLE는 호출 제한기 자체를 쓸 수 없어 요청을 거절한 경우(pk_ 키)에만 Retry-After가 포함됩니다. 그 밖의 503에는 헤더가 없으므로 백오프로 재시도하세요. 두 헤더는 API 레퍼런스의 엔드포인트별 페이지에 나와 있습니다.
엔드포인트별 오류 설명은 API 레퍼런스의 각 페이지에 있습니다.
처리 예제
type ApiError = {
defined: boolean;
code: string;
status: number;
message: string;
data?: { code?: "INSUFFICIENT_CREDIT" | "OVERDRAFT_LIMIT_EXCEEDED"; retryAfterMs?: number };
};
function handleIssueError(error: ApiError) {
switch (error.code) {
case "PAYMENT_REQUIRED":
// 운영 알림: 크레딧 충전 필요 (error.data?.code)
return "잠시 후 다시 시도해 주세요.";
case "CONFLICT":
// 멱등 키를 다른 요청에 재사용했는지 확인
return "요청을 처리할 수 없습니다.";
case "TOO_MANY_REQUESTS":
// error.data?.retryAfterMs(Retry-After 헤더)만큼 기다린 뒤 같은 멱등 키로 재시도
return "시도가 너무 많습니다. 잠시 후 다시 시도해 주세요.";
case "SERVICE_UNAVAILABLE":
case "INTERNAL_SERVER_ERROR":
// 같은 멱등 키로 재시도 (/ko/guides/idempotency)
return "잠시 후 다시 시도해 주세요.";
default:
return "인증번호를 보낼 수 없습니다.";
}
}