> ## Documentation Index
> Fetch the complete documentation index at: https://docs.omnicommerce.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Bulk Product Import API

> Import JSON or CSV products asynchronously, then review generated changes.

Use `POST /api/v1/products/bulk-import` to import products into your organization. Send a `products` array, upload a JSON or CSV file, or reference CSV with `csvText` / `documentId`. The endpoint returns HTTP `202` and a `bulk_...` job ID; processing continues asynchronously. Imported drafts go through the same AI Review workflow as the app.

Each import supports at most **5,000 selected products**, for both `standard` and `enterprise` lanes. This limit applies to JSON, CSV, and document-based imports. Split larger selections into separate imports.

Interactive reference: [API docs](https://docs.omnicommerce.ai/api-reference.md)

## Authentication

Send an OmniCommerce API key or OAuth client token with the `catalog:write` scope:

```http theme={null}
Authorization: Bearer $OMNI_API_KEY
```

API keys and OAuth tokens are bound to a single organization, so you do **not** need to send `organizationId` in the request body or multipart form fields.

Session-authenticated requests (browser cookie) must include `organizationId` in the request body or multipart form fields.

## Import 200 JSON products

Send between **1 and 5,000 products per request**, with a maximum JSON request size of **8 MiB**. A 200-product batch is supported. Send HTTP(S) image URLs; upload image files separately instead of embedding image bytes.

Save a request such as this to `import.json` (extend `products` to include your complete batch):

```json theme={null}
{
  "products": [
    {
      "sku": "LAMP-01",
      "title": "Blue desk lamp",
      "price": 29.9,
      "currency": "SGD",
      "costPerItem": 12,
      "inventory": 50,
      "description": "Adjustable LED desk lamp.",
      "images": ["https://cdn.example.com/lamp-01.jpg"],
      "weight": 1200,
      "dimensions": { "length": 30, "width": 20, "height": 40 }
    }
  ],
  "targetMarketplaces": ["shopify"],
  "marketplaceStoreSelections": { "shopify": "store_123" },
  "dedupPolicy": "keep_first",
  "workflow": {
    "enableRichDescription": true,
    "autoAssignMarketplaceTaxonomy": true,
    "sourceImagesAutomatically": true,
    "enableBackgroundRemoval": true,
    "enableMannequinRemoval": true
  }
}
```

```bash theme={null}
curl "https://your-domain.com/api/v1/products/bulk-import" \
  -H "Authorization: Bearer $OMNI_API_KEY" \
  -H "Content-Type: application/json" \
  --data-binary @import.json
```

The API validates the complete product array before creating an import. JSON fields use the canonical import names, so `fieldMapping` must be omitted. `sku` is required; other supported fields are `title`, `gtin`, `brand`, `description`, `price`, `currency`, `compareAtPrice`, `salePrice`, `costPerItem`, `inventory`, `images`, `category`, `categoryName`, `weight` (grams), and `dimensions` (centimeters). `currency` applies to all price and cost fields on that row. It defaults to SGD for compatibility with existing CSV imports; specify it explicitly for other currencies. Target countries do not convert the supplied prices.

Each object is a flat import row. REST imports preserve rows as independent products; they do not guess variant families or pause for the app’s variant-grouping confirmation. Nested `variants` and unknown fields are rejected rather than silently discarded. Descriptions are limited to 10,000 characters and galleries to 30 HTTP(S) URLs per product. See the API reference for complete field limits.

For larger catalogs, split by both product count and encoded byte size. Submit a few batches concurrently, retain each returned job ID, and poll every job. A `413` response means the payload needs to be split. Do not automatically resubmit a request whose result is unknown: first check whether an import was accepted.

### Upload a JSON file instead

A JSON file may contain an array of products or `{ "products": [...] }`. Supply workflow and target settings as multipart fields; other properties inside the file wrapper are not workflow settings.

```bash theme={null}
curl "https://your-domain.com/api/v1/products/bulk-import" \
  -H "Authorization: Bearer $OMNI_API_KEY" \
  -F "file=@products.json;type=application/json" \
  -F 'targetMarketplaces=["shopify"]' \
  -F 'marketplaceStoreSelections={"shopify":"store_123"}' \
  -F 'workflow={"enableRichDescription":true,"autoAssignMarketplaceTaxonomy":true}'
```

JSON files have the same 5,000-product and 8 MiB limits. Multipart requests are bounded to 9 MiB including form fields and framing.

## Upload a CSV file

```bash theme={null}
curl -X POST "https://your-domain.com/api/v1/products/bulk-import" \
  -H "Authorization: Bearer $OMNI_API_KEY" \
  -F "file=@products.csv" \
  -F 'targetMarketplaces=["shopee"]' \
  -F 'marketplaceStoreSelections={"shopee":"store_123"}' \
  -F 'fieldMapping={"sku":"SKU","title":"Product Name","images":"Image URLs"}'
```

CSV uploads must be `.csv` files up to 2MB.

## Send inline CSV or a stored CSV document

```http theme={null}
POST /api/v1/products/bulk-import
Content-Type: application/json
```

```json theme={null}
{
  "csvText": "sku,title,price,images\nSKU-1,Sample product,19.90,https://cdn.example.com/sku-1.jpg\n",
  "targetMarketplaces": ["shopee"],
  "marketplaceStoreSelections": { "shopee": "store_123" },
  "fieldMapping": {
    "sku": "sku",
    "title": "title",
    "price": "price",
    "images": "images"
  }
}
```

For session-authenticated calls, add `"organizationId": "org_1"` to the JSON body or multipart form.

You can also pass `documentId` instead of `csvText` when the CSV is already stored in the organization's document library.

## Accepted response

```json theme={null}
{
  "ok": true,
  "apiVersion": "developer_v1",
  "operationId": "8f7e8f8f-2b9e-46e6-a2c0-50d0f4612b2d",
  "status": "accepted",
  "jobId": "bulk_7f4f5f0d-2f0a-4f0a-9a2f-8d2f0a7f4f5f",
  "links": {
    "job": "/api/v1/jobs/bulk_7f4f5f0d-2f0a-4f0a-9a2f-8d2f0a7f4f5f"
  },
  "recommendations": [
    "Poll GET /api/v1/jobs/{jobId}?organizationId=org_1 for row progress and completion."
  ],
  "data": {
    "rowCount": 120,
    "fileName": "products.csv",
    "targetMarketplaces": ["shopee"],
    "targetCountries": ["Singapore"],
    "fieldMapping": {
      "sku": "sku",
      "title": "title"
    },
    "counts": {
      "pending": 120
    }
  }
}
```

## Poll job status

```http theme={null}
GET /api/v1/jobs/bulk_7f4f5f0d-2f0a-4f0a-9a2f-8d2f0a7f4f5f?organizationId=org_1
Authorization: Bearer $OMNI_API_KEY
```

The `data` payload includes row counts (`pending`, `enriching`, `applied`, `failed`, `skipped`), progress fields, and the underlying import status. Once review creation finishes, it also includes:

```json theme={null}
{
  "reviewBatchIds": ["review-batch-id"],
  "reviewUrls": ["/app/org_1/review/review-batch-id"],
  "reviewWarnings": []
}
```

Import `completed` means source rows have been imported into draft products. It does **not** mean AI proposals have been approved or marketplace listings published. Open each `reviewUrls` entry to review copy, images, and marketplace taxonomy. Check `failed`, `skipped`, and `reviewWarnings` even when the import is completed; a partial import is not success for every row.

## Workflow options

Set top-level `title` to name the Review job (up to 120 characters). The assistant's `bulk_product_import` tool accepts the same `title`, so an AOP can supply a fixed name or bind a `[[ review_name ]]` input. Omit it to use the default timestamped Review name.

JSON and CSV share the app's organization defaults. Explicit `workflow` values override them: `enableRichDescription`, `sourceImagesAutomatically`, `autoAssignMarketplaceTaxonomy`, `enableBackgroundRemoval`, `enableMannequinRemoval`, `skipImageTagging`, `skipZeroInventoryProducts`, `excludedImageTags`, `enabledBuiltInImageTags`, and `customImageTags` (`{ "name", "description" }` objects).

Annotation settings use top-level `imageAnnotationTemplateId`, `imageAnnotationGalleryTemplateId`, and `imageAnnotationScope` (`primary` or `all_gallery`). Templates must belong to the organization. All supported marketplace identifiers use the same import and review flow; taxonomy depends on the connected stores selected for the import.

The legacy `workflow.skipBackgroundRemoval` is still accepted; explicit `enableBackgroundRemoval` takes precedence. `skipEnrichment` and `enrich: false` remain restricted to super admins. Bulk import does not queue ACO generation. Generate a profile from the product after import. `disableAcoGeneration` is accepted and does not turn generation on.

## Cancel an in-flight import (optional)

```http theme={null}
POST /api/v1/jobs/bulk_7f4f5f0d-2f0a-4f0a-9a2f-8d2f0a7f4f5f/cancel
Authorization: Bearer $OMNI_API_KEY
Content-Type: application/json

{
  "organizationId": "org_1"
}
```

Rows already applied remain in the catalog. Remaining queued rows are skipped.

## Multipart form fields

| Field | Required | Description |
| - | - | - |
| `file` | Yes for file upload | CSV or JSON file |
| `targetMarketplaces` | Yes | JSON array or comma-separated list |
| `organizationId` | Session only | Organization scope for browser session auth. Omit for API keys. |
| `fieldMapping` | No | JSON object string |
| `marketplaceStoreSelections` | Yes | Object mapping marketplace → connected store ID, e.g. `{"shopify":"store_123"}`. At least one connected store is required, with one store per marketplace. Missing or disconnected selections return `400`. Catalog country comes from the selected store, including Shopify India. |
| `selectedRowIndexes` | No | JSON array string |
| `workflow` | No | JSON workflow options |
| `lane` | No | `standard` or `enterprise` |
| `dedupPolicy` | No | Duplicate SKU handling policy |
| `autoApproveThreshold` | No | Number from 0 to 1, or `null` |
| `model` | No | Enrichment model override |

## Job ID format

Developer bulk imports return IDs with the `bulk_` prefix, for example `bulk_7f4f5f0d-2f0a-4f0a-9a2f-8d2f0a7f4f5f`. Use the same job polling and cancellation endpoints as single-product `prod_...` jobs and Look `look_...` jobs.

## Errors

* `400` - invalid JSON, missing product source, invalid field mapping, or product validation failure
* `401` - missing or invalid Bearer token
* `403` - token missing `catalog:write` or wrong organization
* `413` - JSON request, multipart body, or uploaded file too large

### Image exclusion results

Image preparation evaluates every configured `customImageTags` condition explicitly. Matching names are stored exactly in normalized snake case and applied before image cleanup and review selection. Reserved image classifications use a fixed vocabulary, including `brand_card`, `size_chart`, `spec_sheet`, `store_info`, and `model_worn`; descriptive content tags can still vary. A logo on an actual product is not a `brand_card`. `model_worn` covers real people wearing or holding an item, including lifestyle photos; use `mannequin` separately for body forms.

`GET /api/v1/products/{productId}` returns `excludedImages` alongside the kept `images`. Each exclusion includes `url`, `sourceUrl`, original zero-based `position`, `imageTags`, `matchedExcludedTags`, and a `reason`: `excluded_tag`, `size_chart`, `gallery_limit`, `non_product_asset`, or `mirror_unavailable`. Exclusion evidence is saved when import images are applied or an image review is approved. Older imports without a saved audit return an empty array; rerun image preparation to generate evidence. Failed classification must be retried before filtered images can be applied. Explicitly setting `skipImageTagging` skips both classification and tag-based exclusions.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.