---
title: "Errors"
description: "Every error code the API returns, its HTTP status, and what to do about it."
---

> Every apinoa docs page as markdown: https://apinoa.com/llms.txt (all in one file: https://apinoa.com/llms-full.txt)

## Error Format

Every error uses the same envelope: `success: false`, an `error` with a stable `code` and a human-readable
`message`, and the `requestId` to quote if you contact support (also sent as the `x-request-id` header).

```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"
}
```

Branch on `code`, never on `message`: messages may be reworded, codes do not change within `v1`. **A call
that fails is not billed.**

## Marketplace and SNS API

### Your Account

| Status | Code | Meaning |
|---|---|---|
| 401 | `UNAUTHORIZED` | The `x-api-key` header is missing, or the key is revoked, expired or unknown. |
| 402 | `INSUFFICIENT_BALANCE` | The call costs more than the credit left. Top up under **Dashboard > Balance**. |
| 403 | `ACCOUNT_SUSPENDED` | The account is suspended; every key on it is refused. Contact support. |
| 403 | `FORBIDDEN` | The key is valid, but this call is not allowed for it. |

`402` is the one to handle: on a prepaid balance it is the normal end of credit, not a fault. Retrying does
not clear it.

### Your Request

| Status | Code | Meaning |
|---|---|---|
| 400 | `BROWSABLE_ONLY_UNAVAILABLE` | `browsableOnly=true` on a marketplace without that data. Omit the parameter. |
| 400 | `CATEGORY_NOT_BROWSABLE` | The category is a navigation level with no products. Browse one of its children from `categories`. |
| 400 | `CATEGORY_REQUIRED` | The operation's required category was missing. |
| 400 | `CURRENCY_NOT_SUPPORTED` | The marketplace does not price in the currency asked for. |
| 400 | `IMAGE_REQUIRED` | The operation's required image was missing. |
| 400 | `INVALID_CURSOR` | The `cursor` is not one this operation returned. Start again without it. |
| 400 | `INVALID_PARAMS` | A parameter is missing, unknown or out of range. The message names it. |
| 400 | `INVALID_PRODUCT_ID` | The id is not one this marketplace uses. |
| 400 | `PAGE_NOT_SUPPORTED` | The operation pages by `cursor`, not by `page`. |
| 400 | `QUERY_REQUIRED` | The operation's required keyword was missing. |
| 400 | `VIDEO_TOO_LARGE` | The video file is larger than this API returns (64 MB). |
| 404 | `NOT_FOUND` | No such account, post, group, collection, place or app, or it is private, deleted or age-restricted. |
| 404 | `NOT_SHOPIFY_STORE` | `shopify/catalog` was given a site that is not a Shopify store. |
| 404 | `PRODUCT_NOT_FOUND` | The marketplace has no such product. |
| 404 | `REVIEWS_NOT_FOUND` | The product exists and genuinely has no reviews. On Google Shopping: a single store's product, which Google keeps no reviews for. |
| 404 | `STORE_CLOSED` | The Shopify store is password-protected or closed, so its catalog is not public. |
| 404 | `UNKNOWN_MARKETPLACE` | No such platform. `GET /v1/marketplaces` lists them. |
| 404 | `UNKNOWN_OPERATION` | The platform does not have this operation. |

A `400` is never billed.

### Backpressure

| Status | Code | Meaning |
|---|---|---|
| 429 | `CAPACITY_SATURATED` | No capacity for this call right now. Nothing is wrong with the platform. |
| 429 | `MARKETPLACE_SATURATED` | The same, for one platform. |

Both carry a `Retry-After` header in seconds. Wait that long and retry: it is a real estimate, not a
constant. Neither is billed.

### Coverage

| Status | Code | Meaning |
|---|---|---|
| 501 | `OPERATION_UNSUPPORTED` | The operation exists, but not for this input. |
| 501 | `REVIEW_APP_UNSUPPORTED` | The store publishes reviews through an app this API does not read yet. |
| 501 | `REVIEWS_UNREACHABLE` | The reviews exist (a count or an average says so), but none could be read. |

A `501` is deliberately not a `404` and not an empty list: the data exists, and it could not be read.

### Upstream

| Status | Code | Meaning |
|---|---|---|
| 502 | `UPSTREAM_ERROR` | The platform answered with something unusable. |
| 502 | `UPSTREAM_READ_FAILED` | The platform's answer was cut short. |
| 502 | `UPSTREAM_TIMEOUT` | The platform did not answer in time. |
| 502 | `UPSTREAM_TOO_LARGE` | The platform's answer was too large to return. |
| 502 | `UPSTREAM_UNAVAILABLE` | The platform did not answer. |
| 503 | `SERVICE_UNAVAILABLE` | A transient fault on our side. Retry. |

Retry these with exponential backoff. They are the whole vocabulary: a platform failure is always reported
as one of them.

## Platform-Wide Errors

These can be returned on any path.

| Status | Code | Meaning |
|---|---|---|
| 400 | `BAD_REQUEST` | The request could not be read at all: a malformed body or query string. |
| 404 | `NOT_IN_V1` | The path existed before `/v1` and is not part of it. The `successor` field and a `Link: …; rel="successor-version"` header name its replacement. |
| 413 | `PAYLOAD_TOO_LARGE` | The request body is larger than the endpoint accepts. |
| 415 | `UNSUPPORTED_MEDIA_TYPE` | A JSON endpoint was sent something that is not `application/json`. |
| 429 | `QUOTA_EXCEEDED` | The account has no billing arrangement that covers this API. |
| 429 | `RATE_LIMITED` | The key sent more requests per second than its allowance. Wait the `Retry-After` seconds and retry; refused calls are not billed. |
| 500 | `INTERNAL_ERROR` | A fault on our side. Retry, and quote the `requestId` if it persists. |

## Image Translate

| Status | Code | Meaning |
|---|---|---|
| 4xx | `WORKER_USER_FAULT` | The request itself was refused; the status and message say why. |
| 413 | `IMAGE_TOO_LARGE` | The image's pixel dimensions exceed the limit. The message gives both. |
| 422 | `INVALID_IMAGE` | The bytes are not a readable image. |
| 422 | `UNSUPPORTED_IMAGE_FORMAT` | A readable image in a format that is not translated. |
| 502 | `RENDER_FAILED` | The translation could not be drawn back onto the image. |
| 502 | `ROUTING_INCIDENT` | The request could not be processed. Retry. |
| 502 | `TRANSLATION_FAILED` | The text was read but could not be translated. |
| 504 | `NETWORK_ERROR` | The image URL did not answer in time. |

The `FONT_*` codes belong to custom fonts and are listed in
[Custom Fonts](/docs/api-reference/image-translate/custom-fonts). Two can come 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

| Status | Code | Meaning |
|---|---|---|
| 403 | `PROHIBITED_COUNTRY` | Shipping is not available to the requested country. |
| 404 | `ITEM_NOT_FOUND` | AliExpress has no such product. |
| 503 | `TOKEN_EXPIRED` | AliExpress is temporarily unavailable. Retry later. |

AliExpress's own error number, when it gave one, is in the body's `upstreamCode` field and the
`X-Apinoa-Upstream-Code` response header.

## Retrying

Retry only what can succeed on a second try: the `UPSTREAM_*` family, `SERVICE_UNAVAILABLE`,
`INTERNAL_ERROR` and `TOKEN_EXPIRED` with exponential backoff, and the `429` codes after their
`Retry-After`. Never retry a `4xx` other than `429`: it will fail the same way.

```js tab="JavaScript"
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));
  }
}
```

```python tab="Python"
import os
import time

import requests

RETRYABLE = {"CAPACITY_SATURATED", "MARKETPLACE_SATURATED", "RATE_LIMITED", "SERVICE_UNAVAILABLE",
             "INTERNAL_ERROR", "TOKEN_EXPIRED", "UPSTREAM_ERROR", "UPSTREAM_READ_FAILED", "UPSTREAM_TIMEOUT",
             "UPSTREAM_UNAVAILABLE"}

def call(url, params=None, attempts=4):
    for i in range(attempts):
        res = requests.get(url, params=params, headers={"x-api-key": os.environ["APINOA_API_KEY"]})
        body = res.json()
        if body["success"]:
            return body["data"]
        if body["error"]["code"] not in RETRYABLE or i == attempts - 1:
            raise RuntimeError(f'{body["error"]["code"]}: {body["error"]["message"]}')
        time.sleep(float(res.headers.get("retry-after") or 2 ** i))
```

```bash tab="cURL"
for i in 1 2 3 4; do
  status=$(curl -s -o response.json -w "%{http_code}" \
    "https://gateway.apinoa.com/v1/walmart/search?query=wool+socks" \
    -H "x-api-key: $APINOA_API_KEY")
  case "$status" in
    200) cat response.json; break ;;
    429|5??) sleep $((2 ** i)) ;;
    *) cat response.json; break ;;
  esac
done
```
