Product Catalog · Push API

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 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.codeHTTPRetryableMeaning
AUTHENTICATION_FAILED401NoThe key is missing, unknown or revoked.
VENDOR_PAUSED403NoThe vendor is paused; its keys are refused until the merchant resumes it.
VENDOR_KEY_NOT_ALLOWED403NoA vendor key called a route that only the store key may use.
INVALID_REQUEST400NoA malformed parameter, header or body. The issues list names each problem and its path.
IDEMPOTENCY_KEY_REQUIRED400NoA request with an adjust row came without an Idempotency-Key (1 to 200 characters).
NOT_FOUND404NoNo such job, run, product or report, or one outside what this key may reach (a vendor never learns it exists).
PROFILE_NOT_FOUND404NoThe mapping profile named in the request does not exist.
VENDOR_NOT_FOUND404NoThe vendor in the request does not exist.
IDEMPOTENCY_CONFLICT409NoThe Idempotency-Key was used before with a different body.
RUN_IN_PROGRESS409YesAn open full-sync job of the same source refuses its other writes. Retry after it closes.
JOB_CLOSED409NoThe job is closed (or expired) and takes no more batches.
JOB_FULL409NoThe job holds its limit of about 2,000 batches. Close it and open another.
TOO_MANY_OPEN_JOBS409YesThe caller already has 3 open jobs. Close one or wait for one to expire.
REPORT_TOO_LARGE409NoThe job CSV would exceed 900 parts. Stock results can be read paged; for an error CSV, split the feed into smaller jobs.
HANDLE_TAKEN409NoAnother product already has that handle.
IDENTIFIER_LOCKED409NoA rename of a product with no externalId that an import or the API supplies. Rename it by externalId.
REPORT_EXPIRED410NoThe report is gone: reports are kept 30 days after the run finishes.
REQUEST_BODY_TOO_LARGE413NoThe body is over the route’s limit (5 MiB for a batch, 1 MiB for inventory).
MAPPING_INVALID422NoThe mapping profile cannot map these rows.
RATE_LIMITED429YesMore than 600 requests in 60 seconds on one key. Wait Retry-After seconds.
PUSH_QUEUE_FULL429YesThe caller has 2,000 batches waiting to be applied. Wait Retry-After seconds (60).
INTERNAL_ERROR500YesThe catalog failed. Retry with the same Idempotency-Key.
INFRASTRUCTURE_UNAVAILABLE503YesA 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 codeMeaningFix
INVALID_ROWRow couldn't be readMake sure the row has no more cells than the header, and in JSON files that each line is one JSON object.
INVALID_VALUEA value isn't valid for its columnCheck 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_SKURow has no SKUEvery variant row needs a SKU. Fill the SKU column, or map the right column in the profile.
MISSING_PRODUCT_HANDLERow has no product handleFill the handle column, or map the column that identifies the product.
INVALID_GTIN_DROPPEDGTIN isn't valid, so it was left outCheck the digits (8, 12, 13 or 14 with a valid check digit). The rest of the row was imported.
DUPLICATE_SKU_REPLACEDThe same SKU appears again laterThe later row won. Keep one row per SKU.
SCATTERED_PRODUCT_ROWSRows of one product aren't togetherSort 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_VALUERows of one product disagreeRows of the same product gave different product-level values (such as the title); the later row won. Make them match.
PRODUCT_SPLIT_ACROSS_CHUNKSProduct was too long for one batchIt was imported in parts. Fewer variants per product, or fewer duplicate rows, avoids this.
TOO_MANY_OPTIONSToo many optionsA product can have at most 3 options (such as Color, Size, Material).
TOO_MANY_VARIANTSToo many variantsA product can have at most 100 variants. Split it into separate products.
CATEGORY_TOO_DEEPCategory is more than 10 levels deepUse at most 10 levels, separated by " > ".
INVALID_STATUSStatus value wasn't usedUse active or draft. Archiving from a file only happens through a full sync.
IMAGE_COPY_FAILEDImage couldn't be copiedOpen 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_ROWImages without their productNo 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_CURRENCYCurrency code not recognisedUse an active ISO 4217 code, like EUR or GBP, in the column header.
PRICE_LIST_BASE_CURRENCYCurrency is already the base currencyThis variant's base price is already in that currency. Edit the base price instead.
LIST_SALE_WITHOUT_PRICESale price without a priceFill the price column in the same currency, or clear the sale price.
LOCALE_NOT_ENABLEDLanguage is not enabledAdd the language under Storefront → Languages, then import again.
GROUP_NOT_FOUNDCustomer group not foundCreate the customer group first, or fix the name in the column header.
GROUP_KIND_MISMATCHGroup does not take fixed pricesOnly fixed-price groups take prices; use a fixed group or remove this column.
TRANSLATION_TOO_LONGTranslation is too longShorten the translated text to fit the field's length limit.
TRANSLATION_CONFLICTOption text is not uniqueTwo option names, or two values of one option, show the same text in this language; make them different.
EXTERNAL_ID_REQUIREDRow has no externalIdA vendor row needs an externalId, the id your system gives the product.
SKU_CONFLICTSKU belongs to another productEach SKU can sit under one product. Use a different SKU, or push it under the product that owns it.
FIELD_NOT_ALLOWEDA field isn't open to this keyRemove the field from the row; the message names it.
ATTRIBUTE_UNKNOWNUnknown attribute keyCreate the attribute in the catalog first, or remove it from the row.
HANDLE_TAKENHandle is already usedAnother product uses this handle. Pick a different one.
MERCHANT_HOLDHeld by the merchantThe merchant has this product on hold, so it stays a draft. Nothing to fix on your side.
QUANTITY_BELOW_ZEROQuantity would go below zeroThe 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_RANGEQuantity is out of rangeA quantity must stay between -1,000,000,000 and 1,000,000,000.
STOCK_MODE_CONFLICTOne SKU uses two quantity modesSend a SKU once per batch, with one quantityMode.
DUPLICATE_SKU_IN_BATCHSKU appears twice in the batchSend each SKU once per batch.
QUANTITY_MODE_WITHOUT_QUANTITYquantityMode without a quantityAdd 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

CodeWhereMeaning and fix
FULL_SYNC_ARCHIVE_SKIPPEDjob.failureCode on a completed jobThe 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_EXPIREDjob.failureCode on a failed jobThe 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)HTTPA 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 URLThe 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.

Handling push errors
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;
  }
}