Skip to main content

Bulk product imports

Bulk import is a two-phase workflow. ARC first records the import and gives you an upload URL, you upload the file, and ARC stages it and exposes a preview for a human or integration to inspect before the commit is enqueued.

create → upload file → stage → poll until PREVIEW → preview → approve → poll until COMPLETED
↘ cancel (any time before approve)
↘ rollback a COMPLETED import

Prepare the file​

The file is a CSV, XLSX, or XLS spreadsheet with one row per variant and a header row. ARC matches headers by name, ignoring case, spaces and underscores, and looks for the header row in the first 15 rows (so a title banner above it is fine).

A minimal file that creates one product:

sku,name,price,uom
EX-1001,Example widget,12.50,Each
Column (any accepted header)RequiredNotes
sku (also code, variant sku, ean)YesIdentifies the variant. In UPSERT mode an existing SKU is updated. Missing → Missing SKU.
name (also product name, product, model)YesProduct name. Missing → Missing product name. Rows with the same name become variants of one product.
uom (also unit, unit of measure)Yes, unless the workspace has a default unitMust match a unit name in the workspace; otherwise Unknown unit of measure "…". Without it and without a workspace default unit, the row fails with A base unit is required….
price (also list price, unit price)NoSelling price, in the workspace's base currency.
sale price, costNoAlso base-currency amounts.
variant name (also variant)NoName of this variant within the product.
brand, categoryNoFound by name; a name the workspace does not have yet is created.
description, barcode (also upc, gtin), tagsNo
images (also image url)NoImage URLs.
statusNoACTIVE, INACTIVE or DISCONTINUED.
weight, length, width, heightNo
currencyNoISO code of the row's prices. A row in another currency may carry only tier prices (below), never price, cost or sale price — ARC never converts a price.
tier1_min_qty, tier1_price, tier2_min_qty, …NoA quantity price ladder: tier N applies from N's minimum quantity.
attr_<name> (for example attr_size)NoA variant attribute; attr_pack_size becomes the attribute pack size.

Unrecognised columns are ignored. The preview (below) shows how many rows will be created, updated or rejected, with sample errors, before anything is written.

Create the import​

curl --fail-with-body --request POST "$ARC_API_URL/api/service/products/bulk" \
--header "Authorization: Bearer $ARC_API_KEY" \
--header "Content-Type: application/json" \
--data '{
"filename": "products-2026-09-18.csv",
"mode": "UPSERT"
}'

The filename must end in .csv, .xlsx, or .xls. mode defaults to CREATE_ONLY when omitted. The response is:

{
"success": true,
"data": {
"importId": "<import-id>",
"uploadUrl": "<presigned-upload-url>",
"fileRef": "<file-ref>"
}
}

The import is created in status UPLOADED, but no file is attached yet. The GraphQL createProductImport operation is the other supported entry point and returns the same three values.

Upload the file​

Send the file bytes with an HTTP PUT to uploadUrl. Do not add the API key: the URL is presigned and is valid for one hour.

curl --fail-with-body --request PUT "<presigned-upload-url>" \
--header "Content-Type: text/csv" \
--upload-file products-2026-09-18.csv

Send Content-Type: text/csv for a CSV file and Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet for an XLSX or XLS file. The file must be 64 MB or smaller. A larger file is refused at stage, and the import moves to FAILED.

Stage and preview​

curl --fail-with-body --request POST \
"$ARC_API_URL/api/service/products/bulk/<import-id>/stage" \
--header "Authorization: Bearer $ARC_API_KEY"

Stage requires an import in UPLOADED status whose file has been uploaded. It returns { "success": true } as soon as the parse job is queued; parsing and validation run in the background. Poll the status route until data.status leaves the staging states:

curl --fail-with-body \
"$ARC_API_URL/api/service/products/bulk/<import-id>" \
--header "Authorization: Bearer $ARC_API_KEY"
StatusMeaning
UPLOADEDCreated; waiting for the file and the stage call.
MAPPINGStage job is reading the file and mapping columns.
VALIDATINGRows are being validated.
PREVIEWReady for preview and approval.
COMMITTINGApproved; the commit job is writing rows.
COMPLETEDCommit finished. Terminal.
FAILEDStage or commit failed, or the import was cancelled. failureReason says why. Terminal.
ROLLED_BACKA completed import was rolled back. Terminal.

When the status is PREVIEW, read the preview:

curl --fail-with-body \
"$ARC_API_URL/api/service/products/bulk/<import-id>/preview" \
--header "Authorization: Bearer $ARC_API_KEY"

A preview is not a commit. Inspect at least:

  • the import status and mode;
  • rows that will be created versus updated (createCount, updateCount, and the sampleRows sample);
  • validation errors and warnings (errorRows, warningRows, and the sampleErrors sample);
  • the total and valid row counts (totalRows, validRows);
  • rules the pipeline applied for you during validation (autoAppliedRules) and the currency the price tiers will land in (currencySummary).

Approve and monitor​

curl --fail-with-body --request POST \
"$ARC_API_URL/api/service/products/bulk/<import-id>/approve" \
--header "Authorization: Bearer $ARC_API_KEY" \
--header "Content-Type: application/json" \
--data '{"modeOverride":"UPSERT"}'

curl --fail-with-body \
"$ARC_API_URL/api/service/products/bulk/<import-id>" \
--header "Authorization: Bearer $ARC_API_KEY"

Approval is accepted only while the import is in PREVIEW status. It moves the import to COMMITTING and enqueues the commit. Poll with a bounded interval and stop when the status is COMPLETED or FAILED. Persist the last observed status and counts so a process restart can resume monitoring without creating another import.

Cancel an import​

An import that has not started committing (UPLOADED, MAPPING, VALIDATING or PREVIEW) can be cancelled, which frees the workspace's single import slot. It moves to FAILED with the reason Cancelled by user.

curl --fail-with-body --request POST \
"$ARC_API_URL/api/service/products/bulk/<import-id>/cancel" \
--header "Authorization: Bearer $ARC_API_KEY"

An import that is COMMITTING or COMPLETED cannot be cancelled (400); roll a completed import back instead. Staff users can also cancel from the console or with the GraphQL cancelProductImport(importId:) mutation.

Rollback​

curl --fail-with-body --request POST \
"$ARC_API_URL/api/service/products/bulk/<import-id>/rollback" \
--header "Authorization: Bearer $ARC_API_KEY"

Only a COMPLETED import can be rolled back; any other status is refused with 400. Rollback is an operation on the import ledger, not a general-purpose product delete. It removes the products and variants the import created. A created variant that is already used on a document or holds stock is deactivated instead of deleted. Rows the import updated are not reverted. The result reports deletedVariants, deactivatedVariants, deletedProducts, and notRevertedCount. Read it and reconcile before running another import.

Safety rules​

  • Use a dedicated READ_WRITE key with an IP allowlist for CI/import workers.
  • Keep the import id tied to the external migration job id.
  • Never approve a preview that your system did not fetch and validate.
  • A workspace can have only one active import at a time (UPLOADED, MAPPING, VALIDATING, PREVIEW, or COMMITTING). Creating a second one is refused with 400 until the first reaches COMPLETED, FAILED, or ROLLED_BACK. An import you created but never finished still counts as active — cancel it instead of leaving it.
  • An unknown import id returns 400 with the message Import <id> not found, not 404.
  • Do not treat success: true from stage or approve as proof that every row committed; poll status and inspect counts.