API 규약
에러 응답
표준 에러 형식과 HTTP 상태, 에러 코드별 원인과 해결 방법.
에러는 모든 엔드포인트에서 같은 모양으로 옵니다. 분기는 error.code 로 하세요. message 는 사람이 읽는 한국어 문구라 바뀔 수 있습니다.
형식
에러 본문
{
"error": {
"code": "UNPROCESSABLE",
"message": "확정된 청구서만 결제할 수 있습니다. 현재 상태: 작성중",
"detail": {"invoice_status": "DRAFT"},
"request_id": "req_4f1c9a2b7d3e5f60a1b2c3d4"
}
}request_id는 응답 헤더X-Request-Id에도 있습니다. 성공 응답에도 붙습니다. 로그에 남겨 두고, 문의할 때 알려 주세요.detail은 코드마다 내용이 다릅니다. 비어 있으면{}입니다.- 결제사가 보낸 원래 코드는 그대로 내보내지 않습니다. 결제사를 바꿔도 가맹점 코드가 깨지지 않게 하기 위해서입니다.
HTTP 상태
| 상태 | 뜻 |
|---|---|
200 · 201 | 처리됨. 결제 거절도 200 입니다(아래). |
400 | 요청 형식 오류 — 필드 누락·형식 틀림·없는 메서드(405 도 이 형식으로) |
401 | 인증 실패(이유는 알리지 않음) |
403 | 허용 IP 밖에서 호출 |
404 | 없는 자원, 또는 없는 API 경로. 다른 가맹점의 자원도 404 입니다(존재 여부를 알리지 않습니다). |
405 | 그 경로가 받지 않는 메서드. 본문 코드는 INVALID_REQUEST, Allow 헤더와 detail.allowed_methods 에 받는 메서드가 옵니다. |
409 | 같은 멱등키의 요청이 처리 중이거나, 같은 수시결제 동의로 다른 결제가 처리 중 |
422 | 형식은 맞지만 지금 상태에서 할 수 없는 일, 또는 멱등키 재사용 |
429 | 분당 호출 한도 초과 |
500 · 503 | 빌링플러스 쪽 오류·일시 장애. 503 은 Retry-After 가 붙습니다. |
에러 코드
| code | 상태 | 원인과 할 일 |
|---|---|---|
UNAUTHORIZED | 401 | 키·서명·시각·난수 중 하나가 틀렸습니다. 서버 시각이 ±5분 안인지, 서명 경로에 쿼리가 들어갔는지 먼저 보세요 — 흔한 실수 |
FORBIDDEN_IP | 403 | 허용 IP 밖에서 호출했습니다. detail.ip 를 API 연동 화면의 허용 IP 에 추가하세요. |
INVALID_REQUEST | 400 · 405 | 요청 형식 오류이거나 그 경로가 받지 않는 메서드입니다. detail.fields 에 어느 필드가 왜 틀렸는지 옵니다. |
IDEMPOTENCY_KEY_REQUIRED | 400 | POST 에 Idempotency-Key 헤더가 없습니다. |
IDEMPOTENCY_KEY_REUSED | 422 | 같은 멱등키로 다른 본문·경로·메서드를 보냈습니다. 새 요청에는 새 키를 쓰세요. |
REQUEST_IN_PROGRESS | 409 | 앞 요청이 아직 처리 중입니다. Retry-After 초 뒤 같은 키로 다시 보내세요. |
NOT_FOUND | 404 | 없는 회원·청구서·결제를 가리켰거나 API 경로를 잘못 적었습니다. message 에 무엇을 못 찾았는지 나옵니다. |
RATE_LIMITED | 429 | 분당 한도를 넘었습니다. Retry-After 초만큼 기다렸다 보내세요 — 레이트리밋 |
UNPROCESSABLE | 422 | 지금 상태에서는 할 수 없는 요청입니다(확정 전 청구서 결제, 이미 취소된 결제 취소 등). message 를 확인하세요. 수시결제·월별 금액 API 는 detail.reason 에 사유 코드가 옵니다. |
INTERNAL_ERROR | 500 · 503 | 빌링플러스 쪽 오류입니다. 서버 오류 결과는 멱등 저장이 되지 않으므로 같은 멱등키로 다시 보내도 됩니다. 계속되면 request_id 와 함께 문의해 주세요. |
형식 오류(400) 예
400
{
"error": {
"code": "INVALID_REQUEST",
"message": "요청 값이 올바르지 않습니다.",
"detail": {"fields": {"phone": ["휴대전화 번호를 확인해 주세요. 숫자 9~11자리로 입력합니다."]}},
"request_id": "req_9a8b7c6d5e4f3a2b1c0d9e8f"
}
}detail.fields 의 모양은 엔드포인트에 따라 조금 다릅니다(필드 이름 → 문구 목록, 또는 검증 항목 목록). 화면에 그대로 보여 주기보다 로그로 남겨 개발 중에 고치는 용도입니다.
422 의 사유 코드
일부 API 는 422 UNPROCESSABLE 에 detail.reason 을 함께 줍니다. 사유 코드 목록은 각 문서에 있습니다.
- 수시결제 동의 —
LIMIT_ONCE·LIMIT_MONTH·CONSENT_REVOKED·CARD_UNAVAILABLE등 - 월별 청구 금액 —
NOT_VARIABLE·NOT_A_ROUND·ROUND_LOCKED·INVALID_DATE등
결제 승인(POST /api/v1/payments)이 결제사로 나가기 전에 막히면 422 UNPROCESSABLE 의 detail.kind 로 원인을 구분합니다.
| detail.kind | 뜻 | 할 일 |
|---|---|---|
PAYMENT_IN_FLIGHT | 같은 청구서에 결과를 기다리는 결제(PENDING·UNKNOWN)가 있습니다. detail.payment_id · detail.payment_status 에 그 결제가 실립니다. | 다시 승인하지 마세요. 그 결제가 실제로 승인됐을 수 있습니다. GET /api/v1/payments/{payment_id} 로 결과가 확정될 때까지 확인합니다. 실패로 확정된 뒤에만 새 멱등키로 승인합니다. |
PG_CONFIGURATION | 결제사 연결이나 수수료 정책이 아직 준비되지 않았습니다. message 에 이유가 적혀 있습니다. | 다시 보내도 같은 결과입니다. 빌링플러스 화면의 결제사 계약 상태를 확인하세요. |
결제 거절은 에러가 아닙니다
카드 한도 초과·정지 카드 같은 거절은 정상적인 처리 결과입니다. HTTP 200 으로 오고, 결제 객체의 status 가 FAILED, failure_reason 에 빌링플러스 표준 사유가 들어 있습니다.
200 — 거절된 결제(일부)
{
"id": 8812,
"payment_no": "P-2026-1028-0007",
"status": "FAILED",
"amount": 29900,
"failure_reason": "LIMIT_EXCEEDED",
"failure_message": "한도초과",
"approved_at": null
}| status | 뜻 |
|---|---|
PENDING | 요청중 |
APPROVED | 승인완료 |
PARTIAL | 부분취소 |
CANCELED | 취소완료 |
FAILED | 승인실패 |
UNKNOWN | 미확정 — 결제사 응답을 못 받았다. 조회로 확정될 때까지 성공도 실패도 아니다. 이 상태로는 웹훅이 나가지 않는다 |
| failure_reason | 뜻 |
|---|---|
LIMIT_EXCEEDED | 한도초과 |
INSUFFICIENT | 잔액부족 |
INVALID_CARD | 카드오류 |
SUSPENDED | 정지·분실카드 |
EXPIRED | 유효기간 경과 |
TEMPORARY | 일시오류(재시도 가치 있음) |
PG_ERROR | 결제사 오류 |
UNMAPPED | 미분류 — 결제사 코드를 아직 분류하지 못함 |
UNKNOWN 은 실패가 아닙니다. 결제사 응답을 받지 못한 상태라 실제로는 승인됐을 수 있습니다. 다시 승인하지 말고 조회나 웹훅으로 확정을 기다리세요 — 미확정 결제
사유 코드는 앞으로 늘어날 수 있습니다. 모르는 값이 오면 "사유 확인 필요" 처럼 다루고, 성공이나 특정 사유로 단정하지 마세요.
개발자센터