Apinoa Docs

Error Codes

Complete reference of error codes returned by the Apinoa API.

View raw .mdx

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

CodeStatusWhat it means
UNAUTHORIZED401No x-api-key header, or the key is revoked, expired or unknown
INSUFFICIENT_BALANCE402The call costs more than the credit left. Top up at Dashboard > Balance
FORBIDDEN403The key is valid, but this call is not allowed for it
ACCOUNT_SUSPENDED403The 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

CodeStatusWhat it means
CAPACITY_SATURATED429We had no capacity for this call. Nothing is wrong with the marketplace
MARKETPLACE_SATURATED429Same, 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

CodeStatusWhat it means
UNKNOWN_MARKETPLACE404No such marketplace. GET /v1/marketplaces lists them
UNKNOWN_OPERATION404That marketplace does not implement this operation
INVALID_PARAMS400A parameter is missing, unknown or out of range; the message names it
QUERY_REQUIRED IMAGE_REQUIRED CATEGORY_REQUIRED400The operation's one required input was absent
CATEGORY_NOT_BROWSABLE400That category is a navigation hub with no products. Call categories with the same id and browse a child
BROWSABLE_ONLY_UNAVAILABLE400browsableOnly=true on a marketplace we have no measurements for. Omit the parameter to get the full taxonomy
INVALID_CURSOR400The cursor was not one this operation issued. Start again without it
PAGE_NOT_SUPPORTED400That operation pages by cursor, not by page
CURRENCY_NOT_SUPPORTED400The marketplace does not price in the currency asked for
INVALID_PRODUCT_ID400The id is not one this marketplace uses
PRODUCT_NOT_FOUND404The marketplace has no such product
REVIEWS_NOT_FOUND404The product exists and genuinely has no reviews

A 400 is never billed.

Coverage

CodeStatusWhat it means
OPERATION_UNSUPPORTED501The operation exists but not for this input
REVIEW_APP_UNSUPPORTED501The merchant publishes reviews through an app we do not read yet
REVIEWS_UNREACHABLE501The 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

CodeStatusWhat it means
UPSTREAM_ERROR502The marketplace answered with something we could not use
UPSTREAM_UNAVAILABLE502The marketplace did not answer
UPSTREAM_READ_FAILED502The answer was cut short
UPSTREAM_TIMEOUT502The marketplace did not answer in time
SERVICE_UNAVAILABLE503A 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.

CodeStatusWhat it means
BAD_REQUEST400The request could not be read at all: a malformed body, an unparseable query string
UNSUPPORTED_MEDIA_TYPE415A JSON endpoint was sent something that is not application/json
PAYLOAD_TOO_LARGE413The request body is larger than the endpoint accepts
NOT_FOUND404No route at that path
NOT_IN_V1404That 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_LIMITED429The API key sent more requests per second than its allowance. Wait the Retry-After seconds and retry; refused calls are not billed
QUOTA_EXCEEDED429The account has no billing arrangement that covers this API
INTERNAL_ERROR500A fault on our side. Retry, and quote the requestId if it persists

Image Translation Errors

CodeStatusWhat it means
INVALID_IMAGE422The bytes are not a readable image
UNSUPPORTED_IMAGE_FORMAT422A readable image in a format we do not translate
IMAGE_TOO_LARGE413The image's pixel dimensions are larger than this endpoint accepts. The message gives the size it was and the limit
NETWORK_ERROR504The image URL did not answer in time
TRANSLATION_FAILED502The text was read but could not be translated
RENDER_FAILED502The translation could not be drawn back onto the image
ROUTING_INCIDENT502The request could not be processed. Retry
WORKER_USER_FAULT4xxThe 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

CodeStatusWhat it means
ITEM_NOT_FOUND404Product does not exist on the marketplace
PROHIBITED_COUNTRY403Shipping is not available to the requested country
TOKEN_EXPIRED503AliExpress 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

On this page