Skip to main content

Errors

Every non-2xx response returns a JSON body with the same shape. Switch on error (stable), show message (may reword), and log request_id (for support tickets).

{
"error": "insufficient_credits",
"message": "Need 2.6 credits; balance is 0.4",
"request_id": "req_01H8Y3G7Z8mnpqrsw"
}

Error codes

Every code below is stable — safe to switch on. The list is exhaustive: if you see a code not listed here, treat it as internal_error and open a support ticket with the request_id.

4xx — client errors

HTTPerrorWhen it happensYou should
400bad_requestMalformed JSON, missing required field, invalid parameter valueFix the request. Don't retry.
400invalid_barcodeBarcode doesn't match ^[0-9]{8,14}$Normalize before calling. Don't retry.
401unauthenticatedMissing / malformed / expired access tokenRefresh the token via /auth/refresh, then retry.
401invalid_credentialsclient_id + api_key rejected at /auth/tokenCheck env vars. Rotate the key if leaked. Don't retry.
401refresh_token_invalidRefresh token not found, expired, or already usedRe-authenticate with client_id + api_key.
401refresh_token_reusedRefresh token reused — all tokens for this client revokedAlarm loudly. Rotate credentials. This means a leak.
402insufficient_creditsBalance too low to charge for the operationTop up. See credits_remaining in meta for last known balance.
403client_disabledAdmin disabled the client, or it hit expires_atContact admin. Don't retry.
403client_ip_not_allowedRequest came from an IP not in the client's allowlistUpdate allowlist in your account, or call from an allowed IP.
403out_of_scopeBarcode outside your client's scope filterDon't retry. Upgrade your tier if you need broader access.
403signed_url_ip_mismatchSigned image URL requested from a different IP than issued toFetch a fresh URL from the same IP that will consume it.
404not_foundBarcode not in catalogDon't retry. The barcode isn't in our DB.
404variant_not_availableBarcode exists but this variant (e.g. back_clean) doesn'tCheck image_meta to see which variants exist before requesting.
410signed_url_expiredSigned image URL past its 15-min TTLFetch a fresh URL.
410signed_url_consumedSigned image URL already used (they're single-use)Fetch a fresh URL. Don't cache signed URLs on disk.
429rate_limitedHit RPM, RPD, or global limitBack off with exponential jitter. Honor Retry-After header.

5xx — server errors

HTTPerrorWhen it happensYou should
500internal_errorUnhandled exception — we've been pagedRetry with exponential backoff; if persistent, open a ticket.
502upstream_errorOrigin storage transient failureRetry.
503service_unavailableMaintenance window or overloadHonor Retry-After. Do not hammer.
504gateway_timeoutResponse didn't complete in timeRetry, but reduce payload size (fewer barcodes in bulk_check, etc).

Retry policy

Retry on: 500, 502, 503, 504, 429 (with Retry-After).

Don't retry on: any 4xx other than 429. Retrying 401 without refreshing the token, or 400 without changing the request, wastes budget and may get you rate-limited.

Recommended: exponential backoff with jitter starting at 500ms, cap 30s, max 5 retries.

async function withRetry(fn, {maxAttempts = 5} = {}) {
for (let attempt = 0; attempt < maxAttempts; attempt++) {
try {
return await fn();
} catch (err) {
const status = err.response?.status;
const retryable = [429, 500, 502, 503, 504].includes(status);
if (!retryable || attempt === maxAttempts - 1) throw err;
const retryAfter = err.response.headers.get('Retry-After');
const delayMs = retryAfter
? Number(retryAfter) * 1000
: Math.min(500 * 2 ** attempt, 30000) + Math.random() * 250;
await new Promise(r => setTimeout(r, delayMs));
}
}
}

Logging errors for support

When you open a support ticket, include the request_id from the response body. That single value lets us pull the full server-side trace: which endpoint, which client, what parameters, what balance, which node served it. Without a request_id we're guessing.