Error Codes
Complete reference of error codes returned by the Apinoa API.
Error Response Format
All error responses follow a consistent envelope format:
{
"success": false,
"error": {
"code": "ERROR_CODE",
"message": "Human-readable error description"
}
}Gateway endpoints also include a requestId for tracking:
{
"success": false,
"error": {
"code": "INSUFFICIENT_BALANCE",
"message": "Your balance does not cover this call. Top up at https://apinoa.com/dashboard/balance."
},
"requestId": "550e8400e29b41d4a716446655440000"
}Marketplace API Errors
These come from gateway.apinoa.com. Every one carries a requestId — quote it if you contact support.
Your account
| Code | Status | What it means |
|---|---|---|
UNAUTHORIZED | 401 | No x-api-key header, or the key is revoked, expired or unknown |
INSUFFICIENT_BALANCE | 402 | The call costs more than the credit left. Top up at Dashboard > Balance |
FORBIDDEN | 403 | The key is valid, but this call is not allowed for it |
ACCOUNT_SUSPENDED | 403 | The account is suspended: every key on it is refused until the suspension is lifted. Contact support |
402 is the one to handle. Under pay-as-you-go it is the normal end of a balance, not a fault — retrying
will not clear it, and every call until you top up returns the same thing.
Backpressure
| Code | Status | What it means |
|---|---|---|
CAPACITY_SATURATED | 429 | We had no capacity for this call. Nothing is wrong with the marketplace |
MARKETPLACE_SATURATED | 429 | Same, scoped to one marketplace |
Both carry Retry-After in seconds. Wait that long — it is a real estimate, not a constant — and retry.
Neither is billed.
Your request
| Code | Status | What it means |
|---|---|---|
UNKNOWN_MARKETPLACE | 404 | No such marketplace. GET /v1/marketplaces lists them |
UNKNOWN_OPERATION | 404 | That marketplace does not implement this operation |
INVALID_PARAMS | 400 | A parameter is missing, unknown or out of range; the message names it |
QUERY_REQUIRED IMAGE_REQUIRED CATEGORY_REQUIRED | 400 | The operation's one required input was absent |
CATEGORY_NOT_BROWSABLE | 400 | That category is a navigation hub with no products. Call categories with the same id and browse a child |
BROWSABLE_ONLY_UNAVAILABLE | 400 | browsableOnly=true on a marketplace we have no measurements for. Omit the parameter to get the full taxonomy |
INVALID_CURSOR | 400 | The cursor was not one this operation issued. Start again without it |
PAGE_NOT_SUPPORTED | 400 | That operation pages by cursor, not by page |
CURRENCY_NOT_SUPPORTED | 400 | The marketplace does not price in the currency asked for |
INVALID_PRODUCT_ID | 400 | The id is not one this marketplace uses |
PRODUCT_NOT_FOUND | 404 | The marketplace has no such product |
REVIEWS_NOT_FOUND | 404 | The product exists and genuinely has no reviews |
A 400 is never billed.
Coverage
| Code | Status | What it means |
|---|---|---|
OPERATION_UNSUPPORTED | 501 | The operation exists but not for this input |
REVIEW_APP_UNSUPPORTED | 501 | The merchant publishes reviews through an app we do not read yet |
REVIEWS_UNREACHABLE | 501 | The reviews exist — a count or an average says so — but none came back |
A 501 deliberately is not a 404 and not an empty list: the data exists, we could not read it.
Upstream
| Code | Status | What it means |
|---|---|---|
UPSTREAM_ERROR | 502 | The marketplace answered with something we could not use |
UPSTREAM_UNAVAILABLE | 502 | The marketplace did not answer |
UPSTREAM_READ_FAILED | 502 | The answer was cut short |
UPSTREAM_TIMEOUT | 502 | The marketplace did not answer in time |
SERVICE_UNAVAILABLE | 503 | A transient fault on our side. Retry |
A call that delivered nothing is not billed, whatever its status. Retry with exponential backoff.
Every code on this page is the whole vocabulary: a marketplace error is always reported as one of
them, never as a narrower code from further down. Quote the requestId and support can see what the
narrower one was.
Platform Errors
These are not about any one marketplace — they can be returned on any path, for any marketplace.
| Code | Status | What it means |
|---|---|---|
BAD_REQUEST | 400 | The request could not be read at all: a malformed body, an unparseable query string |
UNSUPPORTED_MEDIA_TYPE | 415 | A JSON endpoint was sent something that is not application/json |
PAYLOAD_TOO_LARGE | 413 | The request body is larger than the endpoint accepts |
NOT_FOUND | 404 | No route at that path |
NOT_IN_V1 | 404 | That path existed before /v1 and is not part of it. The body's successor field, and a Link: …; rel="successor-version" header, name the path that replaced it |
RATE_LIMITED | 429 | The API key sent more requests per second than its allowance. Wait the Retry-After seconds and retry; refused calls are not billed |
QUOTA_EXCEEDED | 429 | The account has no billing arrangement that covers this API |
INTERNAL_ERROR | 500 | A fault on our side. Retry, and quote the requestId if it persists |
Image Translation Errors
| Code | Status | What it means |
|---|---|---|
INVALID_IMAGE | 422 | The bytes are not a readable image |
UNSUPPORTED_IMAGE_FORMAT | 422 | A readable image in a format we do not translate |
IMAGE_TOO_LARGE | 413 | The image's pixel dimensions are larger than this endpoint accepts. The message gives the size it was and the limit |
NETWORK_ERROR | 504 | The image URL did not answer in time |
TRANSLATION_FAILED | 502 | The text was read but could not be translated |
RENDER_FAILED | 502 | The translation could not be drawn back onto the image |
ROUTING_INCIDENT | 502 | The request could not be processed. Retry |
WORKER_USER_FAULT | 4xx | The request itself was refused — the status and message say why |
The FONT_* codes belong to custom fonts and are listed with them in
Custom fonts. Two more can reach you from the
translate call itself: FONT_AUTH_REQUIRED (401) when a private font is named without a key that
owns it, and FONT_RESOLVE_ERROR (500) when the font could not be loaded.
AliExpress Errors
| Code | Status | What it means |
|---|---|---|
ITEM_NOT_FOUND | 404 | Product does not exist on the marketplace |
PROHIBITED_COUNTRY | 403 | Shipping is not available to the requested country |
TOKEN_EXPIRED | 503 | AliExpress is temporarily unavailable. Retry later |
AliExpress's own error number, when it gave one, is in the upstreamCode field of the body and in the
X-Apinoa-Upstream-Code response header.
Handling Errors
Retry Strategy
For transient errors (the UPSTREAM_* family, SERVICE_UNAVAILABLE, TOKEN_EXPIRED), implement
exponential backoff. For CAPACITY_SATURATED and MARKETPLACE_SATURATED, wait the Retry-After
the response gives you instead of guessing:
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;
// Do not retry client errors
// INSUFFICIENT_BALANCE will not clear on its own — retrying just spends the retries.
if (["INSUFFICIENT_BALANCE", "UNAUTHORIZED", "FORBIDDEN", "ACCOUNT_SUSPENDED", "INVALID_PARAMS",
"ITEM_NOT_FOUND", "PRODUCT_NOT_FOUND"].includes(code)) {
throw new Error(`${code}: ${error.error.message}`);
}
// Retry transient errors with exponential backoff
if (attempt < maxRetries - 1) {
const delay = Math.pow(2, attempt) * 1000;
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
throw new Error("Max retries exceeded");
}# Simple retry with 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