Errors
Every failed request answers the same envelope with a stable error.code. A failed
row is not a failed request: it is reported in the batch answer and the job's error report.
The envelope
{ "error": { "code": "INVALID_REQUEST", "message": "…", "requestId": "…", "issues": [{ "path": "rows.0.sku", "message": "…" }] } } issues appears on validation errors and names every problem. Quote requestId when you ask for help. A 429 and some 409s carry Retry-After.
Codes
| error.code | HTTP | Retryable | Meaning |
|---|---|---|---|
AUTHENTICATION_FAILED | 401 | No | The key is missing, unknown or revoked. |
VENDOR_PAUSED | 403 | No | The vendor is paused; its keys are refused until the merchant resumes it. |
VENDOR_KEY_NOT_ALLOWED | 403 | No | A vendor key called a route that only the store key may use. |
INVALID_REQUEST | 400 | No | A malformed parameter, header or body. The issues list names each problem and its path. |
IDEMPOTENCY_KEY_REQUIRED | 400 | No | A request with an adjust row came without an Idempotency-Key (1 to 200 characters). |
NOT_FOUND | 404 | No | No such job, run, product or report, or one outside what this key may reach (a vendor never learns it exists). |
PROFILE_NOT_FOUND | 404 | No | The mapping profile named in the request does not exist. |
VENDOR_NOT_FOUND | 404 | No | The vendor in the request does not exist. |
IDEMPOTENCY_CONFLICT | 409 | No | The Idempotency-Key was used before with a different body. |
RUN_IN_PROGRESS | 409 | Yes | An open full-sync job of the same source refuses its other writes. Retry after it closes. |
JOB_CLOSED | 409 | No | The job is closed (or expired) and takes no more batches. |
JOB_FULL | 409 | No | The job holds its limit of about 2,000 batches. Close it and open another. |
TOO_MANY_OPEN_JOBS | 409 | Yes | The caller already has 3 open jobs. Close one or wait for one to expire. |
REPORT_TOO_LARGE | 409 | No | The job CSV would exceed 900 parts. Stock results can be read paged; for an error CSV, split the feed into smaller jobs. |
HANDLE_TAKEN | 409 | No | Another product already has that handle. |
IDENTIFIER_LOCKED | 409 | No | A rename of a product with no externalId that an import or the API supplies. Rename it by externalId. |
REPORT_EXPIRED | 410 | No | The report is gone: reports are kept 30 days after the run finishes. |
REQUEST_BODY_TOO_LARGE | 413 | No | The body is over the route’s limit (5 MiB for a batch, 1 MiB for inventory). |
MAPPING_INVALID | 422 | No | The mapping profile cannot map these rows. |
RATE_LIMITED | 429 | Yes | More than 600 requests in 60 seconds on one key. Wait Retry-After seconds. |
PUSH_QUEUE_FULL | 429 | Yes | The caller has 2,000 batches waiting to be applied. Wait Retry-After seconds (60). |
INTERNAL_ERROR | 500 | Yes | The catalog failed. Retry with the same Idempotency-Key. |
INFRASTRUCTURE_UNAVAILABLE | 503 | Yes | A dependency is briefly unavailable. Retry with the same Idempotency-Key. |
Retryable means the same request can succeed later without a change. Always resend with the same Idempotency-Key.
Row issues
A failed row is not a failed request. A batch answer lists refused rows in issues
as { index, code } (the position in the batch, 0 based; no message) and counts
them in rowsRefused. The job's errors.csv adds
the row number (job-wide, from 1), SKU, field, severity, message and externalId.
Rows refused or failed count in rowsFailed; warnings leave the row applied and count in
rowsWarned. Every code a push can report:
| Row code | Meaning | Fix |
|---|---|---|
INVALID_ROW | Row couldn't be read | Make sure the row has no more cells than the header, and in JSON files that each line is one JSON object. |
INVALID_VALUE | A value isn't valid for its column | Check the value against the column's type; the message names the problem (for example a number like 54.99 or a currency code like USD). If the message says two columns fill the same field, unmap one of them. |
MISSING_SKU | Row has no SKU | Every variant row needs a SKU. Fill the SKU column, or map the right column in the profile. |
MISSING_PRODUCT_HANDLE | Row has no product handle | Fill the handle column, or map the column that identifies the product. |
INVALID_GTIN_DROPPED | GTIN isn't valid, so it was left out | Check the digits (8, 12, 13 or 14 with a valid check digit). The rest of the row was imported. |
DUPLICATE_SKU_REPLACED | The same SKU appears again later | The later row won. Keep one row per SKU. |
SCATTERED_PRODUCT_ROWS | Rows of one product aren't together | Sort the file so each product's rows are next to each other. In merge mode this is a warning and later rows won. In replace mode it is an error and none of the product's rows were applied. |
CONFLICTING_PRODUCT_VALUE | Rows of one product disagree | Rows of the same product gave different product-level values (such as the title); the later row won. Make them match. |
PRODUCT_SPLIT_ACROSS_CHUNKS | Product was too long for one batch | It was imported in parts. Fewer variants per product, or fewer duplicate rows, avoids this. |
TOO_MANY_OPTIONS | Too many options | A product can have at most 3 options (such as Color, Size, Material). |
TOO_MANY_VARIANTS | Too many variants | A product can have at most 100 variants. Split it into separate products. |
CATEGORY_TOO_DEEP | Category is more than 10 levels deep | Use at most 10 levels, separated by " > ". |
INVALID_STATUS | Status value wasn't used | Use active or draft. Archiving from a file only happens through a full sync. |
IMAGE_COPY_FAILED | Image couldn't be copied | Open the link in a browser to check it works. Private, expired or very large images, and non-https links, can't be copied. |
IMAGE_ONLY_ROW | Images without their product | No variant row of this product was in the same part of the file, so the images were skipped. Keep image rows next to their product's rows. |
INVALID_CURRENCY | Currency code not recognised | Use an active ISO 4217 code, like EUR or GBP, in the column header. |
PRICE_LIST_BASE_CURRENCY | Currency is already the base currency | This variant's base price is already in that currency. Edit the base price instead. |
LIST_SALE_WITHOUT_PRICE | Sale price without a price | Fill the price column in the same currency, or clear the sale price. |
LOCALE_NOT_ENABLED | Language is not enabled | Add the language under Storefront → Languages, then import again. |
GROUP_NOT_FOUND | Customer group not found | Create the customer group first, or fix the name in the column header. |
GROUP_KIND_MISMATCH | Group does not take fixed prices | Only fixed-price groups take prices; use a fixed group or remove this column. |
TRANSLATION_TOO_LONG | Translation is too long | Shorten the translated text to fit the field's length limit. |
TRANSLATION_CONFLICT | Option text is not unique | Two option names, or two values of one option, show the same text in this language; make them different. |
EXTERNAL_ID_REQUIRED | Row has no externalId | A vendor row needs an externalId, the id your system gives the product. |
SKU_CONFLICT | SKU belongs to another product | Each SKU can sit under one product. Use a different SKU, or push it under the product that owns it. |
FIELD_NOT_ALLOWED | A field isn't open to this key | Remove the field from the row; the message names it. |
ATTRIBUTE_UNKNOWN | Unknown attribute key | Create the attribute in the catalog first, or remove it from the row. |
HANDLE_TAKEN | Handle is already used | Another product uses this handle. Pick a different one. |
MERCHANT_HOLD | Held by the merchant | The merchant has this product on hold, so it stays a draft. Nothing to fix on your side. |
QUANTITY_BELOW_ZERO | Quantity would go below zero | The adjustment is larger than the stock on hand and the variant does not allow backorders. Send a smaller change, or use set. |
QUANTITY_OUT_OF_RANGE | Quantity is out of range | A quantity must stay between -1,000,000,000 and 1,000,000,000. |
STOCK_MODE_CONFLICT | One SKU uses two quantity modes | Send a SKU once per batch, with one quantityMode. |
DUPLICATE_SKU_IN_BATCH | SKU appears twice in the batch | Send each SKU once per batch. |
QUANTITY_MODE_WITHOUT_QUANTITY | quantityMode without a quantity | Add variant.quantity to the row, or remove quantityMode. |
Warnings are MERCHANT_HOLD, INVALID_GTIN_DROPPED,
CATEGORY_TOO_DEEP, DUPLICATE_SKU_REPLACED and
SCATTERED_PRODUCT_ROWS in merge mode (in replace mode it refuses the product). A currency the store has no price list for, or a locale it has not enabled, refuses a vendor row: for a currency the error is FIELD_NOT_ALLOWED with "The store has no price list in this currency."; for a locale it is LOCALE_NOT_ENABLED. Stock rows in PATCH /inventory report invalid with the stock codes above.
Job failure codes and other codes
| Code | Where | Meaning and fix |
|---|---|---|
FULL_SYNC_ARCHIVE_SKIPPED | job.failureCode on a completed job | The full-sync archive pass did not run: a row was refused or failed, or the job saw no rows. Nothing was archived. Fix the rows and run a new full-sync job with the whole feed. The job and its webhook are still completed: check failureCode. |
JOB_EXPIRED | job.failureCode on a failed job | The job was not closed within 24 hours; its staged batches were discarded and never applied. Send the feed again in a new job. |
VENDOR_ROUTE_UNAVAILABLE (503) | HTTP | A guard that should not occur in production: a route is not yet open to vendor keys. Report the requestId. |
WEBHOOK_URL_INVALID (422) | Setting a webhook URL | The URL is not https or is otherwise not allowed. See Webhooks. |
The same codes, with the exact response and a corrected request, are on Troubleshooting.
In the SDK
The admin client throws the same classes as the rest of @usethrottle/catalog: a CatalogError with the server's code, and a RateLimitedError (with retryAfterSeconds) for every 429. See Errors and rate limits for the
storefront side.
import { createCatalogAdmin, isCatalogError, RateLimitedError } from '@usethrottle/catalog/admin';
const admin = createCatalogAdmin({ key: process.env.CATALOG_ADMIN_KEY ?? '' });
try {
await admin.inventory.set([{ sku: 'HR-GUJI-250', quantity: 40 }]);
} catch (error) {
if (error instanceof RateLimitedError) {
// RATE_LIMITED and PUSH_QUEUE_FULL: wait this long, then send again.
console.log(error.retryAfterSeconds);
} else if (isCatalogError(error)) {
// Branch on the code, and read the per-field problems.
console.log(error.statusCode, error.code, error.issues, error.retryable);
} else {
throw error;
}
}