Errors
Status codes, error shape, and how to handle each kind.
Error response shape
Errors return a JSON object with at minimum:
{
"error": "human-readable message",
"code": "machine-readable code"
}The HTTP status code drives behavior; the error message is for logs; the code is what you branch on.
404 not-found responses and the name_not_supported 400 also carry extra, additive fields to help you (or your agent) self-correct: identifier (the lookup identifier), identifier_type (the inferred or explicitly selected type; null if unknown), reason (a more specific machine code, e.g. upc_not_in_catalog or item_id_not_found), hint (a human-readable did-you-mean), and docs_url. code is unchanged, so existing branching keeps working.
Every GET /v1/products/{id} 404 includes these fields. A retailer-specific hint names the retailer and suggests its product page URL. A 9-digit unscoped item ID hint also explains the Home Depot URL alternative. These responses cost zero credits.
Generic not-found reasons are not_found, item_id_not_found, upc_not_in_catalog (UPC/EAN/GTIN), isbn_not_found, and asin_not_found. Specific reasons take precedence: confirmed_gone, item_id_redirected, not_an_asin, not_yet_cached, url_passthrough_no_data, identity_mismatch, retailer_pending, or retailer_unavailable. Best Buy failures retain their provider-specific reason. Always branch on code first: a 404 may describe an unavailable lookup or an identity mismatch, not a missing product. A Best Buy SKU lookup (7–8 digits, no ?retailer=) that we could not complete is answered 503 retailer_unavailable with a Retry-After header, not a 404 (see the table).
Status code reference
| Status | code | Meaning | Retry? |
|---|---|---|---|
400 | bad_request | Invalid identifier or query param | No (fix client) |
400 | name_not_supported | A product name / free text was sent to this ID-and-URL endpoint (no search-by-name) | No (send a UPC/ID/URL) |
400 | not_a_product_url | A category, search, brand or listing URL was sent; this endpoint looks up one product per call. Send a single product URL (Target: target.com/p/<name>/-/A-<id>). Not charged and not counted against your rate limit. | No (send a product URL) |
401 | unauthorized | Missing / malformed bearer | No (check key) |
403 | forbidden | Key revoked, scope mismatch, or out of quota | No |
404 | not_found | Identifier not in our catalog, or (ASIN lookups) the listing is gone from the retailer. A real verdict — safe to mark the item unavailable. Body carries identifier_type + reason + hint. Walmart item_id lookups: reason: item_id_redirected means Walmart serves a DIFFERENT item's page for the id you sent (a retired or merged id), so we return not-found rather than the other item's data under your id. It says this id has no page of its own, not that the product is gone from Walmart: keep your last known good data and do not mark the item unavailable on this alone. | No |
404 | identity_mismatch | ASIN lookups. The page exists but resolves to a DIFFERENT product (body carries asin_match.matched_asin). We know nothing about the ASIN you sent — this is not a "listing is gone" verdict. Keep your last known good price and stock. | No (deterministic) |
404 | scrape_pending | Not cached yet; a live fetch was started for this product URL (body carries retry_after_seconds) | Yes (retry in ~30-60s) |
404 | retailer_unavailable | The ?retailer= path only. We could not get a usable page from that retailer for this lookup — a provider error, a timeout, or the retailer serving an interstitial instead of the product page. This is not a statement that the retailer does not carry the product; branch on it separately from not_found. Its reason is retailer_unavailable. A failed lookup on a bare 7–8 digit Best Buy SKU is no longer a 404: it is the 503 retailer_unavailable below. | Usually — see the message |
404 | retailer_pending | The retailer is bot-gated and needs our browser extension, which is not released yet | No |
402 | payment_required | Account past due | After billing fixed |
429 | rate_limited | Throttle or monthly quota exceeded | Yes (Retry-After) |
500 | internal_error | Our problem; check status page | Yes (with backoff) |
502 | upstream_unavailable | Walmart backend transient issue | Yes (with backoff) |
503 | retailer_unavailable | Best Buy SKU lookups (a bare 7–8 digit identifier, with no ?retailer=). A 10-character Best Buy product id that cannot be read is answered 404 url_passthrough_no_data instead. We could not complete the Best Buy lookup — a provider error or timeout, Best Buy serving a country-selection page instead of the product page, a spending limit on our side, or a configuration problem on our side. This is not a statement that Best Buy does not carry the product: keep your last known good data. The body carries retryable: true, retailer: "bestbuy", retry_after (seconds, the same value as the Retry-After header), and reason, which names why the lookup failed: provider_error or provider_exception (retry after about 60 seconds), geo_interstitial (about 5 minutes), not_configured (about an hour), or budget_exhausted (a monthly spending limit on our side; Retry-After counts down to the 1st of the next month, UTC, and is never more than one day). Note reason here is a DIFFERENT vocabulary from the reason on a not_found — branch on code first, then on reason. The body also carries the identifier, identifier_type, hint and docs_url fields a 404 has. This code can also appear for a 7–8 digit identifier that is not a Best Buy SKU at all: those are ambiguous with Walmart item IDs, so when our own catalog misses we attempt Best Buy as a fallback, and a failure of that attempt is reported honestly rather than as a definite miss. A Best Buy lookup that completes and finds no product is still a plain 404 not_found. Nothing is charged. | Yes (honor Retry-After) |
503 | lookup_unavailable | ASIN lookups. We could not read the listing — a bot check or a transport failure. Says nothing about the product; body carries retryable: true. | Yes (with backoff) |
503 | service_unavailable | Maintenance or capacity. Also returned, with reason: deadline_exceeded, retryable: true and a Retry-After header, when a product lookup could not finish within our time limit (most often a force_refresh during a traffic peak). With reason: refresh_capacity it is answered at once, before any work starts, because live refreshes are at capacity: wait the Retry-After seconds, then retry, spreading retries out rather than sending them together. With reason: live_read_unavailable and retryable: true, a time-limited force_refresh could not finish its live read in time; retry, or pass allow_stored=true to accept the last stored record, labeled with when it was read. Nothing is charged for any of these. | Yes (honor Retry-After) |
503 | offers_unavailable | Live offers could not be fetched for this item right now. Says nothing about the product. The body carries retry_after (seconds) and the response has a Retry-After header. Not charged. | Yes (honor Retry-After) |
504 | upstream_timeout | Walmart backend slow | Yes (with backoff) |
Customer-safe upstream messages
retailerapi never echoes raw upstream error messages back to your client. We map upstream failures to canonical messages:
404upstream — "Product not found"401/403upstream on product lookups — HTTP502, codeupstream_unavailable, "Service temporarily unavailable". This is a service dependency failure, not a problem with your API key or scopes. Customer authentication and scope failures still return401/403.429upstream — "Rate limited, retry shortly"5xxupstream — "Service temporarily unavailable"
Recommended retry policy
async function callWithRetry(url, opts, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const r = await fetch(url, opts);
if (r.status < 500 && r.status !== 429) return r; // not retriable
if (r.status === 429) {
const retryAfter = Number(r.headers.get('Retry-After') ?? '5');
await sleep(retryAfter * 1000);
continue;
}
if (r.status >= 500 && attempt < maxRetries) {
await sleep(2 ** attempt * 500); // exponential backoff
continue;
}
return r; // give up
}
}Sentry / error tracking
retailerapi's own server errors are tracked in Sentry. We do NOT log your request bodies, only stack traces and route names. If you encounter a persistent 500 on a specific identifier, email hello@retailerapi.com with the identifier and we'll investigate.