오류
모든 4xx/5xx 응답은 동일한 JSON envelope를 가집니다.
{
"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— 내부errors.go상수에서 파생된 소문자 스네이크 케이스 센티넬 이름(예:INVALID_INPUT → "invalid_input"). 릴리스 간 안정적 입니다 — 이것으로 분기하세요.details— 검증 오류 시 채워집니다. 클라이언트가 오류를 입력에 매핑할 수 있도록{ field, message }객체 배열입니다. 그 외에는 생략됩니다.
현재 본문에 trace_id 또는 timestamp를 반환하지 않습니다. 이 페이지의
초기 초안에서 둘 다 약속했지만 — 그것은 희망 사항이었습니다. 서버 로그를
요청과 연관시켜야 하는 경우, 응답 Date 헤더와 게이트웨이 측 레이트 리밋
헤더(X-RateLimit-*)를 캡처하고 지원 티켓에 인용하세요.
게이트웨이 엣지 거부는 다른 형식을 사용합니다. 위의 envelope는
백엔드 서비스가 발생시키는 것입니다. 서비스에 도달하기 전에 게이트웨이에서
거부된 요청 — /b2b/v1/* 호출에 누락/잘못된 X-Signature, 알 수 없는
X-Client-ID, 또는 오래된 X-Timestamp — 는 { "error": "...", "message": "..." } 형태로 돌아옵니다. 여기서 error는 거친 슬러그(unauthorized /
bad_request / service_unavailable)이고 message는 세부 사항을
전달합니다. 숫자 code도, details도 없습니다. 따라서 검증자는 먼저
HTTP 상태로 분기하고, message를 읽고, 요청이 게이트웨이를 통과한
후에만 code/details를 존재로 다루어야 합니다. 예시 게이트웨이 본문
(401):
{ "error": "unauthorized", "message": "invalid signature" }HTTP 상태 → 일반적인 코드
| HTTP | 일반적인 code 값 | 의미 |
|---|---|---|
| 400 | INVALID_INPUT, MISSING_REQUIRED, INVALID_FORMAT, INVALID_LENGTH, INVALID_VALUE, INVALID_USER_STATUS, INVALID_USER_ROLE, PAYMENT_METHOD_NOT_SUPPORTED, AMOUNT_BELOW_MINIMUM | 잘못된 요청 — details를 확인 |
| 401 | INVALID_CREDENTIALS, INVALID_TOKEN, TOKEN_EXPIRED, INVALID_SIGNATURE (B2B); INVALID_OTP, OTP_EXPIRED, SESSION_EXPIRED (대시보드 / JWT만) | 인증 실패 — 잘못된 키, 만료된 타임스탬프, 잘못된 서명. OTP/SESSION_EXPIRED 코드는 대시보드 JWT 경로(/payment/v1/*)에서만 노출됩니다. 순수 B2B 통합에서는 볼 수 없습니다. |
| 402 | INSUFFICIENT_CREDIT | 가맹점 선불 잔액 소진 — 재시도 전 충전 필요 |
| 403 | FORBIDDEN, IP_BLOCKED | 키는 유효하지만 이 호출에 대한 스코프/IP 허용 권한 없음 |
| 404 | NOT_FOUND, RECORD_NOT_FOUND, USER_NOT_FOUND, SESSION_NOT_FOUND | 리소스가 존재하지 않음(또는 가맹점에 대해 존재하지 않음) |
| 409 | ALREADY_EXISTS, USER_ALREADY_EXISTS, SESSION_ALREADY_EXISTS | 다른 본문으로 멱등성 재시도, 또는 상태 머신이 전이를 거부함 |
| 429 | TOO_MANY_REQUESTS, TOO_MANY_ATTEMPTS | 레이트 리밋 도달. 백오프 후 재시도 |
| 500 | INTERNAL_SERVER_ERROR, DATABASE_CONNECTION_ERROR, DATABASE_QUERY_ERROR, DATABASE_TRANSACTION_ERROR, REDIS_CONNECTION_ERROR, REDIS_OPERATION_ERROR, EXTERNAL_SERVICE_ERROR | 저희 측 문제. 백오프로 재시도 안전. (TWILIO_SERVICE_ERROR / SENDGRID_SERVICE_ERROR와 같은 제공자별 코드는 내부적으로 존재하지만 대시보드 측 알림 흐름에만 노출되며 B2B 엔드포인트에는 노출되지 않습니다.) |
| 503 | SERVICE_UNAVAILABLE | 다운스트림 종속성 다운. 백오프 후 재시도 |
전체 코드 레퍼런스
볼 수 있는 전체 code 값 집합(payment-service/pkg/errors/errors.go와 일치):
인증 및 세션 (401)
INVALID_CREDENTIALS— 사용자명/비밀번호 또는 API 키 조합 거부됨INVALID_TOKEN— JWT/세션 토큰이 파싱 불가하거나 변조됨TOKEN_EXPIRED— JWT가exp를 지남INVALID_OTP— OTP 불일치OTP_EXPIRED— OTP가 허용 윈도우 이상 전에 발급됨SESSION_EXPIRED— 대시보드 세션 만료INVALID_SIGNATURE— B2B / 웹훅 호출의 HMAC 서명 불일치
권한 (403)
FORBIDDEN— 인증되었지만 역할/스코프/가맹점 경계가 액션을 차단함IP_BLOCKED— IP가 남용 목록에 있음
찾을 수 없음 (404)
NOT_FOUND— 일반RECORD_NOT_FOUND— 주어진 ID에 대한 행 누락USER_NOT_FOUND— 사용자 조회 실패SESSION_NOT_FOUND— 대시보드 세션 ID 인식 불가
충돌 (409)
ALREADY_EXISTS— 일반USER_ALREADY_EXISTS— 가입이 고유 제약에 걸림SESSION_ALREADY_EXISTS— 중복 세션 삽입
검증 (400)
INVALID_INPUT— 일반.details확인MISSING_REQUIRED— 필수 필드 누락INVALID_FORMAT— 값이 예상 형식과 일치하지 않음(예: UUID, URL, email)INVALID_LENGTH— 값이 너무 짧거나 너무 길음INVALID_VALUE— 값이 허용된 enum/범위를 벗어남INVALID_USER_STATUS— 사용자가 액션을 허용하지 않는 상태INVALID_USER_ROLE— 역할에 액션 권한이 없음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)
DATABASE_CONNECTION_ERROR— DB 도달 불가DATABASE_QUERY_ERROR— 쿼리 플랜이 런타임에 실패DATABASE_TRANSACTION_ERROR— 커밋/롤백 실패REDIS_CONNECTION_ERROR— Redis 도달 불가REDIS_OPERATION_ERROR— Redis 명령 실패EXTERNAL_SERVICE_ERROR— 일반 서드파티 실패(아래에 버킷되지 않은 제공자)TWILIO_SERVICE_ERROR— Twilio SMS / Verify 호출 실패SENDGRID_SERVICE_ERROR— SendGrid 메일 전송 실패INTERNAL_SERVER_ERROR— 예상치 못한 폴스루. 응답Date헤더 +X-RateLimit-*를 캡처하여 문의
가용성 (503)
SERVICE_UNAVAILABLE— 중요한 다운스트림이 비정상 보고 중. 백오프 + 재시도
검증 오류 (400)
문제가 잘못된 요청 본문인 경우 details는 배열이므로 오류를 필드에 다시
매핑할 수 있습니다.
{
"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구분자 누락, 또는 보낸 것과 다른 파싱-후-재문자열화된 본문 서명)
정확한 서명 알고리즘은 인증을 참조하세요.
레이트 리밋 (429)
| 표면 | 제한 |
|---|---|
모든 게이트웨이 경로(/b2b/v1/* 포함) | 게이트웨이의 IP당 — 500 req/min, 공유 버킷. 가맹점당이 아님. |
공개 체크아웃(/checkout/*) | 더 엄격한 서브 버킷 — IP당 20 req/min |
X-RateLimit-Limit과 X-RateLimit-Remaining은 모든 게이트웨이 레이트
리밋 응답(성공뿐 아니라)에 노출됩니다. IP에 대한 실시간 예산으로 다루세요.
레이트 리밋 응답은 Retry-After 헤더(윈도우가 재설정될 때까지의 초)를
포함합니다 — 이를 준수하세요. 폴백으로 지터를 사용한 백오프 — 1s 베이스 +
30s까지 지수적 증가 — 를 적용하세요.
레이트 리밋은 변경될 수 있습니다. 타당한 이유로 이에 도달하는 경우(예: 큰 과거 범위 리컨실리에이션), 문의하세요 — 벌크 엔드포인트는 로드맵에 있습니다.
잔액 부족 (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회 시도 — 웹훅 → 개요와 동일한 스케줄)에서 지수 백오프 재시도를 위해 큐에 들어갑니다. 전달별 전체 이력은 대시보드의 Developers → Webhooks → [endpoint] → Delivery log 패널에 표시됩니다. 여섯 번째 시도가 실패하면 전달이 데드 레터링됩니다 — 대시보드는 서버가 정상화된 후 수동으로 재생할 수 있는 “Failed” 배지를 노출합니다.