Apinoa Docs

에러

API가 돌려주는 모든 에러 코드, HTTP 상태, 그리고 대처 방법.

원본 .mdx 보기

에러 형식

모든 에러는 같은 봉투를 씁니다. success: false, 바뀌지 않는 code와 사람이 읽는 message를 담은 error, 그리고 고객 지원에 문의할 때 알려 줄 requestId(x-request-id 헤더로도 보냅니다)입니다.

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

분기는 message가 아니라 code로 하세요. 메시지는 문구가 바뀔 수 있지만, 코드는 v1 안에서 바뀌지 않습니다. 실패한 호출은 과금되지 않습니다.

Marketplace API와 SNS API

계정

상태코드의미
401UNAUTHORIZEDx-api-key 헤더가 없거나, 폐기·만료되었거나 알 수 없는 키입니다.
402INSUFFICIENT_BALANCE호출 비용이 남은 크레딧보다 큽니다. 대시보드 > 잔액에서 충전하세요.
403ACCOUNT_SUSPENDED계정이 정지되어 그 계정의 모든 키가 거부됩니다. 고객 지원에 문의하세요.
403FORBIDDEN키는 유효하지만 이 키로는 이 호출을 할 수 없습니다.

처리해야 할 것은 402입니다. 선불 잔액에서는 결함이 아니라 크레딧이 정상적으로 소진된 것입니다. 다시 시도해도 해결되지 않습니다.

요청

상태코드의미
400BROWSABLE_ONLY_UNAVAILABLE해당 데이터가 없는 마켓플레이스에 browsableOnly=true를 보냈습니다. 파라미터를 빼세요.
400CATEGORY_NOT_BROWSABLE상품이 없는 탐색용 단계의 카테고리입니다. categories에서 하위 카테고리를 골라 탐색하세요.
400CATEGORY_REQUIRED오퍼레이션에 필요한 카테고리가 없습니다.
400CURRENCY_NOT_SUPPORTED마켓플레이스가 요청한 통화로 가격을 매기지 않습니다.
400IMAGE_REQUIRED오퍼레이션에 필요한 이미지가 없습니다.
400INVALID_CURSOR이 오퍼레이션이 돌려준 cursor가 아닙니다. 커서 없이 처음부터 다시 호출하세요.
400INVALID_PARAMS파라미터가 없거나, 알 수 없거나, 범위를 벗어났습니다. 메시지에 이름이 나옵니다.
400INVALID_PRODUCT_ID이 마켓플레이스가 쓰는 형식의 id가 아닙니다.
400PAGE_NOT_SUPPORTED이 오퍼레이션은 page가 아니라 cursor로 페이지를 넘깁니다.
400QUERY_REQUIRED오퍼레이션에 필요한 키워드가 없습니다.
400VIDEO_TOO_LARGE영상 파일이 이 API가 돌려주는 크기(64 MB)보다 큽니다.
404NOT_FOUND계정·게시물·그룹·컬렉션·장소·앱이 없거나, 비공개·삭제·연령 제한 상태입니다.
404NOT_SHOPIFY_STOREshopify/catalog에 Shopify 스토어가 아닌 사이트를 넘겼습니다.
404PRODUCT_NOT_FOUND마켓플레이스에 해당 상품이 없습니다.
404REVIEWS_NOT_FOUND상품은 있지만 실제로 리뷰가 없습니다. Google Shopping에서는 한 상점의 상품이라 Google이 리뷰를 보관하지 않는 경우입니다.
404STORE_CLOSEDShopify 스토어가 비밀번호로 보호되거나 닫혀 있어 카탈로그가 공개되지 않습니다.
404UNKNOWN_MARKETPLACE해당 플랫폼이 없습니다. GET /v1/marketplaces가 목록을 보여 줍니다.
404UNKNOWN_OPERATION해당 플랫폼에 이 오퍼레이션이 없습니다.

400은 과금되지 않습니다.

백프레셔

상태코드의미
429CAPACITY_SATURATED지금은 이 호출을 처리할 여유가 없습니다. 플랫폼에는 문제가 없습니다.
429MARKETPLACE_SATURATED위와 같으며, 한 플랫폼에 해당합니다.

둘 다 초 단위의 Retry-After 헤더를 담습니다. 그만큼 기다린 뒤 다시 시도하세요. 고정값이 아니라 실제 추정치입니다. 둘 다 과금되지 않습니다.

지원 범위

상태코드의미
501OPERATION_UNSUPPORTED오퍼레이션은 있지만 이 입력에는 지원하지 않습니다.
501REVIEW_APP_UNSUPPORTED스토어가 이 API가 아직 읽지 못하는 앱으로 리뷰를 게시합니다.
501REVIEWS_UNREACHABLE리뷰는 있지만(개수나 평균이 그렇게 말합니다) 하나도 읽을 수 없었습니다.

501은 일부러 404나 빈 목록으로 응답하지 않는 것입니다. 데이터는 있지만 읽을 수 없었다는 뜻입니다.

업스트림

상태코드의미
502UPSTREAM_ERROR플랫폼이 쓸 수 없는 응답을 보냈습니다.
502UPSTREAM_READ_FAILED플랫폼의 응답이 중간에 끊겼습니다.
502UPSTREAM_TIMEOUT플랫폼이 제때 응답하지 않았습니다.
502UPSTREAM_TOO_LARGE플랫폼의 응답이 너무 커서 돌려줄 수 없습니다.
502UPSTREAM_UNAVAILABLE플랫폼이 응답하지 않았습니다.
503SERVICE_UNAVAILABLE우리 쪽의 일시적인 장애입니다. 다시 시도하세요.

이 코드들은 지수 백오프로 다시 시도하세요. 이것이 전부입니다. 플랫폼 장애는 항상 이 중 하나로 보고됩니다.

플랫폼 공통 에러

어느 경로에서든 돌아올 수 있습니다.

상태코드의미
400BAD_REQUEST요청을 전혀 읽을 수 없습니다. 본문이나 쿼리 문자열의 형식이 잘못되었습니다.
404NOT_IN_V1/v1 이전에 있던 경로로, /v1에는 없습니다. successor 필드와 Link: …; rel="successor-version" 헤더가 대체 경로를 알려 줍니다.
413PAYLOAD_TOO_LARGE요청 본문이 엔드포인트가 받는 크기보다 큽니다.
415UNSUPPORTED_MEDIA_TYPEJSON 엔드포인트에 application/json이 아닌 것을 보냈습니다.
429QUOTA_EXCEEDED계정에 이 API를 쓸 수 있는 과금 방식이 없습니다.
429RATE_LIMITED키가 허용된 초당 요청 수를 넘었습니다. Retry-After 초만큼 기다린 뒤 다시 시도하세요. 거부된 호출은 과금되지 않습니다.
500INTERNAL_ERROR우리 쪽의 장애입니다. 다시 시도하고, 계속되면 requestId를 알려 주세요.

Image Translate

상태코드의미
4xxWORKER_USER_FAULT요청 자체가 거부되었습니다. 상태와 메시지에 이유가 나옵니다.
413IMAGE_TOO_LARGE이미지의 픽셀 크기가 한도를 넘습니다. 메시지에 둘 다 나옵니다.
422INVALID_IMAGE읽을 수 있는 이미지 데이터가 아닙니다.
422UNSUPPORTED_IMAGE_FORMAT읽을 수는 있지만 번역하지 않는 형식의 이미지입니다.
502RENDER_FAILED번역문을 이미지에 다시 그릴 수 없었습니다.
502ROUTING_INCIDENT요청을 처리할 수 없었습니다. 다시 시도하세요.
502TRANSLATION_FAILED텍스트는 읽었지만 번역할 수 없었습니다.
504NETWORK_ERROR이미지 URL이 제때 응답하지 않았습니다.

FONT_* 코드는 맞춤 폰트에 속하며 Custom Fonts에 정리되어 있습니다. 그중 두 개는 번역 호출 자체에서도 돌아올 수 있습니다. 비공개 폰트를 그 폰트를 소유한 키 없이 지정하면 FONT_AUTH_REQUIRED(401), 폰트를 불러올 수 없으면 FONT_RESOLVE_ERROR(500)입니다.

AliExpress

상태코드의미
403PROHIBITED_COUNTRY요청한 국가로는 배송할 수 없습니다.
404ITEM_NOT_FOUNDAliExpress에 해당 상품이 없습니다.
503TOKEN_EXPIREDAliExpress를 일시적으로 사용할 수 없습니다. 나중에 다시 시도하세요.

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

재시도

다시 시도해서 성공할 수 있는 것만 재시도하세요. UPSTREAM_* 계열, SERVICE_UNAVAILABLE, INTERNAL_ERROR, TOKEN_EXPIRED는 지수 백오프로, 429 코드는 Retry-After만큼 기다린 뒤 재시도합니다. 429 이외의 4xx는 재시도하지 마세요. 똑같이 실패합니다.

const RETRYABLE = new Set(["CAPACITY_SATURATED", "MARKETPLACE_SATURATED", "RATE_LIMITED", "SERVICE_UNAVAILABLE",
  "INTERNAL_ERROR", "TOKEN_EXPIRED", "UPSTREAM_ERROR", "UPSTREAM_READ_FAILED", "UPSTREAM_TIMEOUT", "UPSTREAM_UNAVAILABLE"]);

async function call(url, init = {}, attempts = 4) {
  for (let i = 0; ; i++) {
    const res = await fetch(url, { ...init, headers: { ...init.headers, "x-api-key": process.env.APINOA_API_KEY } });
    const body = await res.json();
    if (body.success) return body.data;
    if (!RETRYABLE.has(body.error.code) || i === attempts - 1) throw new Error(`${body.error.code}: ${body.error.message}`);
    const wait = Number(res.headers.get("retry-after")) || 2 ** i;
    await new Promise((r) => setTimeout(r, wait * 1000));
  }
}

이 페이지 내용