Errors
Every error code the API returns, its HTTP status, and what to do about it.
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).
{
"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 or collection, 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. |
| 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. 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.
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));
}
}