에러 코드
Apinoa API에서 반환하는 에러 코드 전체 레퍼런스입니다.
에러 응답 형식
모든 에러 응답은 일관된 엔벨로프 형식을 따릅니다:
{
"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가 포함되니, 지원팀에 문의할 때 함께 알려주세요.
계정
| 코드 | 상태 | 의미 |
|---|---|---|
UNAUTHORIZED | 401 | x-api-key 헤더가 없거나, 키가 폐기·만료되었거나 알 수 없는 키 |
INSUFFICIENT_BALANCE | 402 | 호출 비용이 남은 잔액보다 큼. 대시보드 > 잔액에서 충전하세요 |
FORBIDDEN | 403 | 키는 유효하지만 이 호출은 허용되지 않음 |
ACCOUNT_SUSPENDED | 403 | 계정이 정지됨: 정지가 해제될 때까지 이 계정의 모든 키가 거부됩니다. 고객 지원팀에 문의하세요 |
반드시 처리해야 할 것은 402입니다. 사용량 기반 과금에서 이는 오류가 아니라 잔액이 소진된 정상적인 상태입니다 —
재시도로는 해결되지 않으며, 충전하기 전까지 모든 호출이 같은 응답을 반환합니다.
백프레셔
| 코드 | 상태 | 의미 |
|---|---|---|
CAPACITY_SATURATED | 429 | 이 호출을 처리할 여유 용량이 없었음. 마켓플레이스 쪽 문제는 아닙니다 |
MARKETPLACE_SATURATED | 429 | 위와 같으나 특정 마켓플레이스에 한정됨 |
두 에러 모두 초 단위의 Retry-After를 포함합니다. 고정값이 아니라 실제 추정치이니 그만큼 기다린 뒤 재시도하세요.
둘 다 과금되지 않습니다.
요청
| 코드 | 상태 | 의미 |
|---|---|---|
UNKNOWN_MARKETPLACE | 404 | 존재하지 않는 마켓플레이스. GET /v1/marketplaces로 목록을 확인하세요 |
UNKNOWN_OPERATION | 404 | 해당 마켓플레이스가 이 오퍼레이션을 지원하지 않음 |
INVALID_PARAMS | 400 | 파라미터가 없거나, 알 수 없거나, 범위를 벗어남. 메시지에 해당 파라미터 이름이 나옵니다 |
QUERY_REQUIRED IMAGE_REQUIRED CATEGORY_REQUIRED | 400 | 오퍼레이션의 필수 입력이 빠짐 |
CATEGORY_NOT_BROWSABLE | 400 | 상품이 없는 탐색용 상위 카테고리. 같은 id로 categories를 호출해 하위 카테고리를 탐색하세요 |
BROWSABLE_ONLY_UNAVAILABLE | 400 | 측정 데이터가 없는 마켓플레이스에 browsableOnly=true를 사용함. 파라미터를 빼면 전체 분류를 받습니다 |
INVALID_CURSOR | 400 | 이 오퍼레이션이 발급한 cursor가 아님. 커서 없이 다시 시작하세요 |
PAGE_NOT_SUPPORTED | 400 | 이 오퍼레이션은 page가 아니라 cursor로 페이지를 넘깁니다 |
CURRENCY_NOT_SUPPORTED | 400 | 마켓플레이스가 요청한 통화로 가격을 제공하지 않음 |
INVALID_PRODUCT_ID | 400 | 이 마켓플레이스가 쓰는 형식의 id가 아님 |
PRODUCT_NOT_FOUND | 404 | 마켓플레이스에 해당 상품이 없음 |
REVIEWS_NOT_FOUND | 404 | 상품은 존재하며 실제로 리뷰가 하나도 없음 |
400은 과금되지 않습니다.
지원 범위
| 코드 | 상태 | 의미 |
|---|---|---|
OPERATION_UNSUPPORTED | 501 | 오퍼레이션은 있지만 이 입력에는 지원되지 않음 |
REVIEW_APP_UNSUPPORTED | 501 | 판매자가 아직 지원하지 않는 앱으로 리뷰를 게시함 |
REVIEWS_UNREACHABLE | 501 | 리뷰 개수나 평점으로 보아 리뷰가 존재하지만, 하나도 가져오지 못함 |
501은 의도적으로 404나 빈 목록과 구분됩니다. 데이터는 존재하지만 읽어오지 못했다는 뜻입니다.
업스트림
| 코드 | 상태 | 의미 |
|---|---|---|
UPSTREAM_ERROR | 502 | 마켓플레이스가 사용할 수 없는 응답을 반환함 |
UPSTREAM_UNAVAILABLE | 502 | 마켓플레이스가 응답하지 않음 |
UPSTREAM_READ_FAILED | 502 | 응답이 중간에 끊김 |
UPSTREAM_TIMEOUT | 502 | 마켓플레이스가 제시간에 응답하지 않음 |
SERVICE_UNAVAILABLE | 503 | 저희 쪽의 일시적 오류. 재시도하세요 |
아무것도 전달하지 못한 호출은 상태 코드와 관계없이 과금되지 않습니다. 지수 백오프로 재시도하세요.
이 문서에 적힌 코드가 전부입니다. 마켓플레이스 오류는 언제나 이 중 하나로 전달되며, 더 세부적인 코드가
그대로 나가지는 않습니다. requestId를 알려주시면 지원팀이 세부 내용을 확인할 수 있습니다.
플랫폼 에러
특정 마켓플레이스와 무관하게, 어떤 경로에서든 어느 마켓플레이스에든 돌아올 수 있는 에러입니다.
| 코드 | 상태 | 의미 |
|---|---|---|
BAD_REQUEST | 400 | 요청 자체를 읽을 수 없음: 형식이 잘못된 본문, 해석할 수 없는 쿼리 문자열 |
UNSUPPORTED_MEDIA_TYPE | 415 | JSON 엔드포인트에 application/json이 아닌 본문을 보냄 |
PAYLOAD_TOO_LARGE | 413 | 요청 본문이 이 엔드포인트가 받는 크기를 넘음 |
NOT_FOUND | 404 | 해당 경로에 라우트가 없음 |
NOT_IN_V1 | 404 | /v1 이전에 있던 경로로, v1에는 포함되지 않음. 본문의 successor 필드와 Link: …; rel="successor-version" 헤더가 대체 경로를 알려줍니다 |
RATE_LIMITED | 429 | API 키의 초당 요청 수가 허용치를 넘음. Retry-After 초만큼 기다린 뒤 다시 시도하세요. 거절된 호출은 과금되지 않습니다 |
QUOTA_EXCEEDED | 429 | 이 API를 사용할 수 있는 과금 설정이 계정에 없음 |
INTERNAL_ERROR | 500 | 저희 쪽 오류. 재시도하고, 계속되면 requestId를 알려주세요 |
이미지 번역 에러
| 코드 | 상태 | 의미 |
|---|---|---|
INVALID_IMAGE | 422 | 이미지로 읽을 수 없는 바이트 |
UNSUPPORTED_IMAGE_FORMAT | 422 | 읽을 수는 있으나 번역을 지원하지 않는 형식 |
IMAGE_TOO_LARGE | 413 | 이미지의 픽셀 크기가 이 엔드포인트의 허용 범위를 넘음. 메시지에 실제 크기와 상한이 함께 표시됩니다 |
NETWORK_ERROR | 504 | 이미지 URL이 제시간에 응답하지 않음 |
TRANSLATION_FAILED | 502 | 텍스트는 읽었으나 번역하지 못함 |
RENDER_FAILED | 502 | 번역문을 이미지에 다시 그리지 못함 |
ROUTING_INCIDENT | 502 | 요청을 처리하지 못함. 재시도하세요 |
WORKER_USER_FAULT | 4xx | 요청 자체가 거부됨 — 상태 코드와 메시지가 이유를 설명합니다 |
FONT_* 코드는 맞춤 폰트에 속하며
맞춤 폰트 문서에 정리되어 있습니다. 번역 호출에서
직접 반환될 수 있는 것이 둘 더 있습니다. 비공개 폰트를 소유하지 않은 키로 지정했을 때의
FONT_AUTH_REQUIRED(401), 폰트를 불러오지 못했을 때의 FONT_RESOLVE_ERROR(500)입니다.
AliExpress 에러
| 코드 | 상태 | 의미 |
|---|---|---|
ITEM_NOT_FOUND | 404 | 마켓플레이스에 해당 상품이 존재하지 않음 |
PROHIBITED_COUNTRY | 403 | 요청한 국가로의 배송 불가 |
TOKEN_EXPIRED | 503 | AliExpress를 일시적으로 사용할 수 없음. 나중에 재시도하세요 |
AliExpress 자체 에러 번호가 있는 경우, 응답 본문의 upstreamCode 필드와 X-Apinoa-Upstream-Code 응답 헤더에
담겨 있습니다.
에러 처리
재시도 전략
일시적 에러(UPSTREAM_* 계열, SERVICE_UNAVAILABLE, TOKEN_EXPIRED)는 지수 백오프를 구현하세요.
CAPACITY_SATURATED와 MARKETPLACE_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