Apinoa Docs

에러 코드

Apinoa API에서 반환하는 에러 코드 전체 레퍼런스입니다.

원본 .mdx 보기

에러 응답 형식

모든 에러 응답은 일관된 엔벨로프 형식을 따릅니다:

{
  "success": false,
  "error": {
    "code": "ERROR_CODE",
    "message": "사람이 읽을 수 있는 에러 설명"
  }
}

게이트웨이 엔드포인트는 추적용 requestId도 포함합니다:

{
  "success": false,
  "error": {
    "code": "INSUFFICIENT_BALANCE",
    "message": "Your balance does not cover this call. Top up at https://apinoa.com/dashboard/balance."
  },
  "requestId": "550e8400e29b41d4a716446655440000"
}

마켓플레이스 API 에러

gateway.apinoa.com에서 반환되는 에러입니다. 모든 에러에 requestId가 포함되니, 지원팀에 문의할 때 함께 알려주세요.

계정

코드상태의미
UNAUTHORIZED401x-api-key 헤더가 없거나, 키가 폐기·만료되었거나 알 수 없는 키
INSUFFICIENT_BALANCE402호출 비용이 남은 잔액보다 큼. 대시보드 > 잔액에서 충전하세요
FORBIDDEN403키는 유효하지만 이 호출은 허용되지 않음
ACCOUNT_SUSPENDED403계정이 정지됨: 정지가 해제될 때까지 이 계정의 모든 키가 거부됩니다. 고객 지원팀에 문의하세요

반드시 처리해야 할 것은 402입니다. 사용량 기반 과금에서 이는 오류가 아니라 잔액이 소진된 정상적인 상태입니다 — 재시도로는 해결되지 않으며, 충전하기 전까지 모든 호출이 같은 응답을 반환합니다.

백프레셔

코드상태의미
CAPACITY_SATURATED429이 호출을 처리할 여유 용량이 없었음. 마켓플레이스 쪽 문제는 아닙니다
MARKETPLACE_SATURATED429위와 같으나 특정 마켓플레이스에 한정됨

두 에러 모두 초 단위의 Retry-After를 포함합니다. 고정값이 아니라 실제 추정치이니 그만큼 기다린 뒤 재시도하세요. 둘 다 과금되지 않습니다.

요청

코드상태의미
UNKNOWN_MARKETPLACE404존재하지 않는 마켓플레이스. GET /v1/marketplaces로 목록을 확인하세요
UNKNOWN_OPERATION404해당 마켓플레이스가 이 오퍼레이션을 지원하지 않음
INVALID_PARAMS400파라미터가 없거나, 알 수 없거나, 범위를 벗어남. 메시지에 해당 파라미터 이름이 나옵니다
QUERY_REQUIRED IMAGE_REQUIRED CATEGORY_REQUIRED400오퍼레이션의 필수 입력이 빠짐
CATEGORY_NOT_BROWSABLE400상품이 없는 탐색용 상위 카테고리. 같은 id로 categories를 호출해 하위 카테고리를 탐색하세요
BROWSABLE_ONLY_UNAVAILABLE400측정 데이터가 없는 마켓플레이스에 browsableOnly=true를 사용함. 파라미터를 빼면 전체 분류를 받습니다
INVALID_CURSOR400이 오퍼레이션이 발급한 cursor가 아님. 커서 없이 다시 시작하세요
PAGE_NOT_SUPPORTED400이 오퍼레이션은 page가 아니라 cursor로 페이지를 넘깁니다
CURRENCY_NOT_SUPPORTED400마켓플레이스가 요청한 통화로 가격을 제공하지 않음
INVALID_PRODUCT_ID400이 마켓플레이스가 쓰는 형식의 id가 아님
PRODUCT_NOT_FOUND404마켓플레이스에 해당 상품이 없음
REVIEWS_NOT_FOUND404상품은 존재하며 실제로 리뷰가 하나도 없음

400은 과금되지 않습니다.

지원 범위

코드상태의미
OPERATION_UNSUPPORTED501오퍼레이션은 있지만 이 입력에는 지원되지 않음
REVIEW_APP_UNSUPPORTED501판매자가 아직 지원하지 않는 앱으로 리뷰를 게시함
REVIEWS_UNREACHABLE501리뷰 개수나 평점으로 보아 리뷰가 존재하지만, 하나도 가져오지 못함

501은 의도적으로 404나 빈 목록과 구분됩니다. 데이터는 존재하지만 읽어오지 못했다는 뜻입니다.

업스트림

코드상태의미
UPSTREAM_ERROR502마켓플레이스가 사용할 수 없는 응답을 반환함
UPSTREAM_UNAVAILABLE502마켓플레이스가 응답하지 않음
UPSTREAM_READ_FAILED502응답이 중간에 끊김
UPSTREAM_TIMEOUT502마켓플레이스가 제시간에 응답하지 않음
SERVICE_UNAVAILABLE503저희 쪽의 일시적 오류. 재시도하세요

아무것도 전달하지 못한 호출은 상태 코드와 관계없이 과금되지 않습니다. 지수 백오프로 재시도하세요. 이 문서에 적힌 코드가 전부입니다. 마켓플레이스 오류는 언제나 이 중 하나로 전달되며, 더 세부적인 코드가 그대로 나가지는 않습니다. requestId를 알려주시면 지원팀이 세부 내용을 확인할 수 있습니다.

플랫폼 에러

특정 마켓플레이스와 무관하게, 어떤 경로에서든 어느 마켓플레이스에든 돌아올 수 있는 에러입니다.

코드상태의미
BAD_REQUEST400요청 자체를 읽을 수 없음: 형식이 잘못된 본문, 해석할 수 없는 쿼리 문자열
UNSUPPORTED_MEDIA_TYPE415JSON 엔드포인트에 application/json이 아닌 본문을 보냄
PAYLOAD_TOO_LARGE413요청 본문이 이 엔드포인트가 받는 크기를 넘음
NOT_FOUND404해당 경로에 라우트가 없음
NOT_IN_V1404/v1 이전에 있던 경로로, v1에는 포함되지 않음. 본문의 successor 필드와 Link: …; rel="successor-version" 헤더가 대체 경로를 알려줍니다
RATE_LIMITED429API 키의 초당 요청 수가 허용치를 넘음. Retry-After 초만큼 기다린 뒤 다시 시도하세요. 거절된 호출은 과금되지 않습니다
QUOTA_EXCEEDED429이 API를 사용할 수 있는 과금 설정이 계정에 없음
INTERNAL_ERROR500저희 쪽 오류. 재시도하고, 계속되면 requestId를 알려주세요

이미지 번역 에러

코드상태의미
INVALID_IMAGE422이미지로 읽을 수 없는 바이트
UNSUPPORTED_IMAGE_FORMAT422읽을 수는 있으나 번역을 지원하지 않는 형식
IMAGE_TOO_LARGE413이미지의 픽셀 크기가 이 엔드포인트의 허용 범위를 넘음. 메시지에 실제 크기와 상한이 함께 표시됩니다
NETWORK_ERROR504이미지 URL이 제시간에 응답하지 않음
TRANSLATION_FAILED502텍스트는 읽었으나 번역하지 못함
RENDER_FAILED502번역문을 이미지에 다시 그리지 못함
ROUTING_INCIDENT502요청을 처리하지 못함. 재시도하세요
WORKER_USER_FAULT4xx요청 자체가 거부됨 — 상태 코드와 메시지가 이유를 설명합니다

FONT_* 코드는 맞춤 폰트에 속하며 맞춤 폰트 문서에 정리되어 있습니다. 번역 호출에서 직접 반환될 수 있는 것이 둘 더 있습니다. 비공개 폰트를 소유하지 않은 키로 지정했을 때의 FONT_AUTH_REQUIRED(401), 폰트를 불러오지 못했을 때의 FONT_RESOLVE_ERROR(500)입니다.

AliExpress 에러

코드상태의미
ITEM_NOT_FOUND404마켓플레이스에 해당 상품이 존재하지 않음
PROHIBITED_COUNTRY403요청한 국가로의 배송 불가
TOKEN_EXPIRED503AliExpress를 일시적으로 사용할 수 없음. 나중에 재시도하세요

AliExpress 자체 에러 번호가 있는 경우, 응답 본문의 upstreamCode 필드와 X-Apinoa-Upstream-Code 응답 헤더에 담겨 있습니다.

에러 처리

재시도 전략

일시적 에러(UPSTREAM_* 계열, SERVICE_UNAVAILABLE, TOKEN_EXPIRED)는 지수 백오프를 구현하세요. CAPACITY_SATURATEDMARKETPLACE_SATURATED는 추측하지 말고 응답에 담긴 Retry-After만큼 기다리세요:

async function fetchWithRetry(url, options, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    const response = await fetch(url, options);

    if (response.ok) {
      return response.json();
    }

    const error = await response.json();
    const code = error.error?.code;

    // 클라이언트 에러는 재시도하지 않음
    // INSUFFICIENT_BALANCE는 저절로 해결되지 않음 — 재시도해도 재시도 횟수만 소모됨
    if (["INSUFFICIENT_BALANCE", "UNAUTHORIZED", "FORBIDDEN", "ACCOUNT_SUSPENDED", "INVALID_PARAMS",
         "ITEM_NOT_FOUND", "PRODUCT_NOT_FOUND"].includes(code)) {
      throw new Error(`${code}: ${error.error.message}`);
    }

    // 일시적 에러는 지수 백오프로 재시도
    if (attempt < maxRetries - 1) {
      const delay = Math.pow(2, attempt) * 1000;
      await new Promise((resolve) => setTimeout(resolve, delay));
    }
  }

  throw new Error("Max retries exceeded");
}
# curl을 사용한 간단한 재시도
for i in 1 2 3; do
  response=$(curl -s -w "\n%{http_code}" -X POST \
    https://gateway.apinoa.com/v1/aliexpress/search \
    -H "Content-Type: application/json" \
    -H "x-api-key: YOUR_API_KEY" \
    -d '{"query": "phone case"}')

  http_code=$(echo "$response" | tail -1)

  if [ "$http_code" -eq 200 ]; then
    echo "$response" | head -1
    break
  fi

  sleep $((2 ** i))
done

이 페이지 내용