에러 처리

API 에러 코드와 처리 방법을 안내합니다.

ONDA Platform Engineering 관리

에러 처리

ONDA Channel API의 에러 응답 구조와 처리 방법을 안내합니다.

에러 응답 형식

모든 에러 응답은 일관된 JSON 구조를 따릅니다:

{
  "error": {
    "code": "ERROR_CODE",
    "message": "사람이 읽을 수 있는 에러 메시지",
    "details": {
      // 추가 컨텍스트 정보 (선택)
    }
  },
  "meta": {
    "request_id": "550e8400-e29b-41d4-a716-446655440000",
    "timestamp": "2026-02-08T10:30:00Z"
  }
}
필드타입설명
error.codestring기계가 읽을 수 있는 에러 코드 (UPPER_SNAKE_CASE)
error.messagestring사람이 읽을 수 있는 에러 설명
error.detailsobject에러 관련 추가 정보 (선택)
meta.request_idstring요청 추적용 고유 ID
meta.timestampstring에러 발생 시각 (ISO 8601)

단, POST /oauth/token은 OAuth 2.0 표준 형식({error, error_description})으로 에러를 반환합니다. 인증 가이드를 참고하세요.

HTTP 상태 코드

2xx - 성공

코드의미사용 케이스
200 OK요청 성공GET, PATCH, PUT 성공
201 Created리소스 생성됨POST /bookings 성공

4xx - 클라이언트 에러

코드의미주요 원인
400 Bad Request잘못된 요청필수 파라미터 누락, 형식 오류, 토큰 형식 오류
401 Unauthorized인증 실패토큰/API 키 없음, 만료, 무효
402 Payment Required잔액 부족가용 잔액 < 예약 입금가
403 Forbidden권한 없음scope 부족, 다른 앱의 리소스 접근
404 Not Found리소스 없음잘못된 ID, 삭제된 리소스
409 Conflict충돌이미 취소된 예약, 상태 전이 불가
422 Unprocessable Entity처리 불가book_token 무효/만료
429 Too Many Requests요청 과다Rate limit 초과

5xx - 서버 에러

코드의미조치
500 Internal Server Error서버 오류재시도 또는 지원팀 문의
501 Not Implemented미구현 기능voucher / tax-invoices 엔드포인트 (추후 제공)
502 Bad Gateway업스트림(Hub) 오류재시도
503 Service Unavailable서비스 불가잠시 후 재시도

공통 에러 코드

인증 관련 (401)

UNAUTHORIZED

{
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Authentication required"
  },
  "meta": {
    "request_id": "550e8400-e29b-41d4-a716-446655440000",
    "timestamp": "2026-02-08T10:30:00Z"
  }
}

원인: 인증 정보(X-Api-Key 또는 Authorization: Bearer) 누락

해결 방법: 모든 요청에 인증 헤더를 포함하세요.

INVALID_API_KEY / INVALID_TOKEN / TOKEN_EXPIRED

원인: API 키 또는 Access Token이 유효하지 않거나 만료됨

해결 방법:

def api_call_with_token_refresh(url, token_manager, **kwargs):
    """토큰 만료 시 재발급 후 재시도"""
    headers = {"Authorization": f"Bearer {token_manager.get_token()}"}
    response = requests.get(url, headers=headers, **kwargs)

    if response.status_code == 401:
        # oat_ 토큰은 30분 후 만료 — client_credentials로 재발급
        token_manager.refresh()
        headers = {"Authorization": f"Bearer {token_manager.get_token()}"}
        response = requests.get(url, headers=headers, **kwargs)

    return response

권한 관련 (403)

SCOPE_DENIED

{
  "error": {
    "code": "SCOPE_DENIED",
    "message": "필요한 권한이 없습니다."
  },
  "meta": {
    "request_id": "550e8400-e29b-41d4-a716-446655440002",
    "timestamp": "2026-02-08T10:30:00Z"
  }
}

원인: 토큰/API 키의 scope가 부족함 (deny-by-default)

해결 방법: 필요한 scope를 포함하여 토큰을 재발급하거나, 대시보드에서 API 키 권한을 조정하세요.

FORBIDDEN

원인: 다른 앱이 소유한 리소스(예약 등)에 접근

해결 방법: 해당 리소스를 생성한 앱의 자격 증명으로 호출하세요.

검증 관련 (400)

INVALID_REQUEST

{
  "error": {
    "code": "INVALID_REQUEST",
    "message": "book_token is required"
  },
  "meta": {
    "request_id": "550e8400-e29b-41d4-a716-446655440004",
    "timestamp": "2026-02-08T10:30:00Z"
  }
}

원인: 필수 파라미터 누락(book_token, booker_name, token 등) 또는 형식 오류

해결 방법: 에러 메시지에 표기된 파라미터를 보완하여 재요청하세요.

INVALID_TOKEN / TOKEN_EXPIRED (price-check, 400)

원인: GET /search/price-checkprice_check_token이 무효이거나 만료됨

해결 방법: GET /search/availability/{propertyId}를 다시 호출하여 새 price_check_token을 발급받으세요.

리소스 관련 (404)

NOT_FOUND / BOOKING_NOT_FOUND

{
  "error": {
    "code": "BOOKING_NOT_FOUND",
    "message": "Booking not found"
  },
  "meta": {
    "request_id": "550e8400-e29b-41d4-a716-446655440005",
    "timestamp": "2026-02-08T10:30:00Z"
  }
}

원인: 요청한 리소스가 존재하지 않음

해결 방법: ID를 확인하거나 목록 API로 유효한 ID를 조회하세요.

예약/결제 관련 (402, 409, 422)

INSUFFICIENT_BALANCE (402)

원인: 운영 환경에서 가용 잔액이 예약 입금가보다 적음

해결 방법: 대시보드에서 보증금을 충전한 후 재시도하세요.

BALANCE_SUSPENDED (403)

원인: 가용 잔액이 기준 미달이어서 판매가 비활성화됨

해결 방법: 보증금을 충전하면 판매가 재개됩니다.

BOOKING_TOKEN_INVALID (422)

{
  "error": {
    "code": "BOOKING_TOKEN_INVALID",
    "message": "book_token is invalid or expired"
  },
  "meta": {
    "request_id": "550e8400-e29b-41d4-a716-446655440006",
    "timestamp": "2026-02-08T10:30:00Z"
  }
}

원인: book_token이 만료되었거나 위·변조됨

해결 방법: GET /search/price-check를 다시 호출하여 새 book_token을 발급받으세요.

BOOKING_NOT_CANCELLABLE (409)

원인: 현재 상태에서 취소할 수 없는 예약

해결 방법: GET /bookings/{id}/cancellation-policy로 취소 가능 여부를 확인하고 고객에게 안내하세요.

BOOKING_ALREADY_CANCELLED (409)

{
  "error": {
    "code": "BOOKING_ALREADY_CANCELLED",
    "message": "이미 취소된 예약입니다."
  },
  "meta": {
    "request_id": "550e8400-e29b-41d4-a716-446655440009",
    "timestamp": "2026-02-08T10:30:00Z"
  }
}

원인: 이미 취소된 예약에 취소를 다시 요청함

해결 방법: 멱등 처리 — 이미 원하는 상태이므로 성공으로 간주해도 됩니다.

BOOKING_NOT_MODIFIABLE (409) / NO_CHANGES (400)

원인: 수정 불가 상태의 예약(BOOKING_NOT_MODIFIABLE) 또는 변경할 내용 없음(NO_CHANGES)

해결 방법: 예약 상태를 확인하고, 수정 가능한 필드(투숙객 정보)만 보내세요.

업스트림 관련 (502)

HUB_UPSTREAM_ERROR

원인: ONDA Hub 업스트림 응답 오류 (검색/숙소 조회 시)

해결 방법: 지수 백오프로 재시도하세요. 지속되면 request_id와 함께 지원팀에 문의하세요.

가격 변동과 재고 소진은 에러가 아닙니다. GET /search/price-check는 가격이 변했거나 매진된 경우에도 200 OK를 반환하며, 응답의 status 필드가 matched / price_changed / sold_out 중 하나로 표시됩니다. price_changed면 새 가격을 고객에게 확인받은 뒤 함께 발급된 새 book_token으로 예약하고, sold_out이면 다른 객실을 제안하세요.

Rate Limiting (429)

RATE_LIMIT_EXCEEDED

{
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "요청이 너무 많습니다. 잠시 후 다시 시도해 주세요."
  },
  "meta": {
    "request_id": "550e8400-e29b-41d4-a716-446655440010",
    "timestamp": "2026-02-08T10:30:00Z"
  }
}

Rate Limit 정책

전체 엔드포인트에 단일 분당 한도가 적용됩니다 (엔드포인트별 차등 없음):

환경/등급제한
Sandbox60 요청/분
Starter (운영)600 요청/분

토큰 발급(POST /oauth/token)은 별도로 IP당 60초에 20회로 제한됩니다.

Rate Limit 헤더

응답 헤더에 현재 상태가 포함됩니다:

X-RateLimit-Limit: 600
X-RateLimit-Remaining: 542
X-RateLimit-Reset: 1709370060

구현 예시:

import time

def api_call_with_rate_limit(url, headers, **kwargs):
    """Rate limit을 고려한 API 호출"""
    response = requests.get(url, headers=headers, **kwargs)

    if response.status_code == 429:
        # Retry-After 헤더 확인
        retry_after = int(response.headers.get("Retry-After", 60))
        print(f"Rate limit exceeded. Retrying after {retry_after}s...")
        time.sleep(retry_after)
        # 재시도
        response = requests.get(url, headers=headers, **kwargs)

    # Rate limit 상태 로깅
    remaining = response.headers.get("X-RateLimit-Remaining")
    if remaining and int(remaining) < 60:
        print(f"Warning: Only {remaining} requests remaining")

    return response

재시도 전략

지수 백오프

import time
import random

def api_call_with_exponential_backoff(
    func,
    max_retries=5,
    base_delay=1,
    max_delay=32
):
    """지수 백오프를 사용한 재시도"""
    for attempt in range(max_retries):
        try:
            response = func()

            # 성공
            if response.status_code < 500:
                return response

            # 서버 에러 (5xx)
            if attempt < max_retries - 1:
                # 지수 백오프 계산
                delay = min(base_delay * (2 ** attempt), max_delay)
                # Jitter 추가 (동시 재시도 방지)
                delay += random.uniform(0, delay * 0.1)

                print(f"Attempt {attempt + 1} failed. Retrying in {delay:.2f}s...")
                time.sleep(delay)
            else:
                # 최대 재시도 횟수 초과
                return response

        except requests.exceptions.RequestException as e:
            if attempt < max_retries - 1:
                delay = min(base_delay * (2 ** attempt), max_delay)
                print(f"Request failed: {e}. Retrying in {delay:.2f}s...")
                time.sleep(delay)
            else:
                raise

    return None

# 사용 예시
response = api_call_with_exponential_backoff(
    lambda: requests.get(
        "https://api.tport.dev/channel/v1/properties",
        headers=headers
    )
)

재시도 가능한 에러

다음 에러는 재시도할 가치가 있습니다:

상태 코드재시도 권장전략
429 Too Many Requests✅ 예Retry-After 헤더 사용
500 Internal Server Error✅ 예지수 백오프
502 Bad Gateway✅ 예지수 백오프
503 Service Unavailable✅ 예지수 백오프
400 Bad Request❌ 아니오요청 수정 필요
401 Unauthorized❌ 아니오토큰 재발급 필요
404 Not Found❌ 아니오재시도 불필요
422 Unprocessable Entity❌ 아니오새 book_token 발급 필요

POST /bookings 재시도는 주의하세요. 타임아웃으로 응답을 받지 못한 경우 예약이 생성되었을 수 있습니다. 재시도 전에 GET /bookings?affiliate_reference_id={내부 예약번호}로 중복 여부를 확인하세요.

에러 로깅 권장 사항

로깅할 정보

import logging

logger = logging.getLogger(__name__)

def log_api_error(response):
    """API 에러 로깅"""
    error_data = response.json()
    error = error_data.get("error", {})
    meta = error_data.get("meta", {})

    logger.error(
        "API Error",
        extra={
            "status_code": response.status_code,
            "error_code": error.get("code"),
            "error_message": error.get("message"),
            "request_id": meta.get("request_id"),
            "url": response.url,
            "method": response.request.method,
            "timestamp": meta.get("timestamp"),
            "details": error.get("details"),
        }
    )

모니터링 메트릭

다음 메트릭을 추적하세요:

  • 에러율: 전체 요청 대비 에러 비율
  • 에러 타입: 에러 코드별 발생 빈도
  • 응답 시간: API 응답 시간 분포
  • Rate limit 사용량: 제한 대비 사용량

에러 처리 체크리스트

  • 모든 API 호출에 에러 처리 구현
  • 401 에러 시 자동 토큰 재발급
  • 429 에러 시 재시도 로직 구현
  • 5xx 에러 시 지수 백오프 재시도
  • price-check status 필드(price_changed/sold_out) 분기 처리
  • 모든 에러를 로그에 기록 (request_id 포함)
  • 중요 에러는 알림 발송
  • 에러율 및 응답 시간 모니터링

다음 단계