---
title: "Batch create/upsert products"
method: POST
path: "/products/batch"
---

# Batch create/upsert products

`POST /products/batch`

## Request

- Base URL: `https://www.merchkit.com/api/v1`
- URL: `https://www.merchkit.com/api/v1/products/batch`
- Auth: HTTP bearer

Up to 100 items in one call. With `merge_key`, every item gets upsert semantics (create-or-update by that key); without it, pure creates. Items are processed independently and the response is PER-ITEM: **HTTP 200 does NOT mean every item succeeded** — check each `status` (`created` | `updated` | `error`) and surface `error.message`/`error.field_errors` for failures. A 5,000-SKU ERP delta push is 50 calls with `merge_key: "sku"`.

Scope: `write:products`.

## Request body

- object
  - `merge_key` string: Optional shared merge key — when present, items upsert instead of create.
  - `items` object[], required
    - `parent_id` string, uuid, nullable
    - `attributes` AttributeWriteMap, required: Map of attribute key → new value (sparse — only keys being written). Scalars set the value; `null` OR `""` clears it (the key then disappears from reads). A single-reference key accepts a bare UUID string or an `{id}` object; a list-reference key accepts an array of either — list writes REPLACE the current set. Read stubs re-sent verbatim are valid writes (extra stub fields like `type`/`label` are ignored). Unknown keys are a 400; select values must be one of the attribute's `acceptable_values`.

## Response `200`

Per-item results, in input order, under a `summary` that makes "N of M failed" a single field read. Check every item — a 200 only means the batch was processed. Route retries on `error.retriable`, never on message text. Summaries from re-runs must not be summed (an idempotent retry reports updates for rows the first attempt created).

- object
  - `summary` BatchSummary, required: Batch-level counts. `has_errors` is the cheap "anything to look at?" check; clean batches can skip the per-item scan. Do not sum summaries across idempotent re-runs of the same batch.
    - `total` integer, required
    - `created` integer, required
    - `updated` integer, required
    - `failed` integer, required
    - `has_errors` boolean, required
  - `data` BatchItemResult[], required
    - `index` integer, required: Position in the request `items`.
    - `merge_value` string: Trimmed echo of `attributes[merge_key]` (present when the batch has a merge_key).
    - `id` string, uuid: The created/updated entity id (absent on error).
    - `status` 'created' | 'updated' | 'error', required
    - `error` object
      - `code` string, required: Platform error code (same vocabulary as the top-level error envelope). Fine-grained causes are in field_errors[].issue.
      - `retriable` boolean, required: Whether re-submitting the same item unchanged can succeed. Route retry queues on this, never on message text.
      - `message` string, required
      - `field_errors` FieldError[]
        - `field` string, required
        - `issue` string, required: Machine-readable issue code, e.g. `not_a_defined_attribute`.
        - `acceptable_values` string[]: For select-value violations: the allowed values.

## Other responses

- `400`: Malformed batch (empty items, >100 items, bad shape).

---

[API](https://skmtc.dev/merchkit/apis/merchkit-api.md) - [All operations](https://skmtc.dev/merchkit/apis/merchkit-api/llms.txt) - [OpenAPI document](https://skmtc.dev/merchkit/apis/merchkit-api/revisions/3062ca814757?raw)
