Apinoa Docs

Errors

Every error code the API returns, its HTTP status, and what to do about it.

View raw .mdx

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

StatusCodeMeaning
401UNAUTHORIZEDThe x-api-key header is missing, or the key is revoked, expired or unknown.
402INSUFFICIENT_BALANCEThe call costs more than the credit left. Top up under Dashboard > Balance.
403ACCOUNT_SUSPENDEDThe account is suspended; every key on it is refused. Contact support.
403FORBIDDENThe 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

StatusCodeMeaning
400BROWSABLE_ONLY_UNAVAILABLEbrowsableOnly=true on a marketplace without that data. Omit the parameter.
400CATEGORY_NOT_BROWSABLEThe category is a navigation level with no products. Browse one of its children from categories.
400CATEGORY_REQUIREDThe operation's required category was missing.
400CURRENCY_NOT_SUPPORTEDThe marketplace does not price in the currency asked for.
400IMAGE_REQUIREDThe operation's required image was missing.
400INVALID_CURSORThe cursor is not one this operation returned. Start again without it.
400INVALID_PARAMSA parameter is missing, unknown or out of range. The message names it.
400INVALID_PRODUCT_IDThe id is not one this marketplace uses.
400PAGE_NOT_SUPPORTEDThe operation pages by cursor, not by page.
400QUERY_REQUIREDThe operation's required keyword was missing.
400VIDEO_TOO_LARGEThe video file is larger than this API returns (64 MB).
404NOT_FOUNDNo such account, post, group or collection, or it is private, deleted or age-restricted.
404NOT_SHOPIFY_STOREshopify/catalog was given a site that is not a Shopify store.
404PRODUCT_NOT_FOUNDThe marketplace has no such product.
404REVIEWS_NOT_FOUNDThe product exists and genuinely has no reviews.
404STORE_CLOSEDThe Shopify store is password-protected or closed, so its catalog is not public.
404UNKNOWN_MARKETPLACENo such platform. GET /v1/marketplaces lists them.
404UNKNOWN_OPERATIONThe platform does not have this operation.

A 400 is never billed.

Backpressure

StatusCodeMeaning
429CAPACITY_SATURATEDNo capacity for this call right now. Nothing is wrong with the platform.
429MARKETPLACE_SATURATEDThe 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

StatusCodeMeaning
501OPERATION_UNSUPPORTEDThe operation exists, but not for this input.
501REVIEW_APP_UNSUPPORTEDThe store publishes reviews through an app this API does not read yet.
501REVIEWS_UNREACHABLEThe 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

StatusCodeMeaning
502UPSTREAM_ERRORThe platform answered with something unusable.
502UPSTREAM_READ_FAILEDThe platform's answer was cut short.
502UPSTREAM_TIMEOUTThe platform did not answer in time.
502UPSTREAM_TOO_LARGEThe platform's answer was too large to return.
502UPSTREAM_UNAVAILABLEThe platform did not answer.
503SERVICE_UNAVAILABLEA 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.

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

Image Translate

StatusCodeMeaning
4xxWORKER_USER_FAULTThe request itself was refused; the status and message say why.
413IMAGE_TOO_LARGEThe image's pixel dimensions exceed the limit. The message gives both.
422INVALID_IMAGEThe bytes are not a readable image.
422UNSUPPORTED_IMAGE_FORMATA readable image in a format that is not translated.
502RENDER_FAILEDThe translation could not be drawn back onto the image.
502ROUTING_INCIDENTThe request could not be processed. Retry.
502TRANSLATION_FAILEDThe text was read but could not be translated.
504NETWORK_ERRORThe 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

StatusCodeMeaning
403PROHIBITED_COUNTRYShipping is not available to the requested country.
404ITEM_NOT_FOUNDAliExpress has no such product.
503TOKEN_EXPIREDAliExpress 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));
  }
}

On this page