---
title: "Error Codes"
description: "Complete reference of error codes returned by the Apinoa API."
---

## Error Response Format

All error responses follow a consistent envelope format:

```json
{
  "success": false,
  "error": {
    "code": "ERROR_CODE",
    "message": "Human-readable error description"
  }
}
```

Gateway endpoints also include a `requestId` for tracking:

```json
{
  "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](/docs/api-reference/image-translate/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:

```javascript
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");
}
```

```bash
# 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
```
