# Push Products

> Upsert products (mode = replace_all runs tombstone products outside the run after the last batch).

Source: https://shopassist.coec.dk/docs/api/reference/catalog/push-products · Updated 2026-11-03

`POST /v1/stores/{store_id}/catalog/products:batch`

- Scope: `catalog:write`
- Rate limit: 600 requests/min per API key (test keys: 60/min); catalog batches 120/min per store

Upsert products (`mode = replace_all` runs tombstone products outside the run after the last batch).

## Parameters [#parameters]

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `store_id` | path | `string` | yes |  |
| `Idempotency-Key` | header | `string` | yes | Replays return the original |

## Request body (ProductsBatch) [#request-body]

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `batch_index` | `integer` | no | ≥ 0 · ≤ 9999 |
| `is_last_batch` | `boolean` | no |  |
| `mode` | `"upsert" \| "replace_all"` | no | default "upsert" |
| `products` | `CatalogProductInput[]` | yes | CatalogProductInput items (validated individually; invalid items are reported in rejected[]) · ≥ 1 items · ≤ 500 items |
| `products[].attributes` | `CatalogAttributeInput[]` | no |  |
| `products[].attributes[].code` | `string` | yes | 1–128 characters |
| `products[].attributes[].label` | `string` | yes | 1–255 characters |
| `products[].attributes[].values` | `string[]` | yes | ≤ 100 items |
| `products[].brand` | `string \| null` | no | ≤ 255 characters |
| `products[].category_external_ids` | `string[]` | no |  |
| `products[].description_html` | `string \| null` | no | Sanitized server-side |
| `products[].description_text` | `string \| null` | no |  |
| `products[].external_id` | `string` | yes | 1–128 characters |
| `products[].handle` | `string \| null` | no | ≤ 255 characters |
| `products[].images` | `CatalogImageInput[]` | no |  |
| `products[].images[].alt` | `string` | no | ≤ 500 characters |
| `products[].images[].url` | `string` | yes | format uri · ≤ 2048 characters |
| `products[].images[].variant_external_ids` | `string[]` | no |  |
| `products[].is_published` | `boolean` | no | default true |
| `products[].product_type` | `string \| null` | no | ≤ 255 characters |
| `products[].status` | `"active" \| "draft" \| "archived"` | no | default "active" |
| `products[].tags` | `string[]` | no |  |
| `products[].title` | `string` | yes | 1–255 characters |
| `products[].translations` | `CatalogTranslationInput[]` | no |  |
| `products[].translations[].description_text` | `string \| null` | no |  |
| `products[].translations[].locale` | `string` | yes | 2–35 characters · pattern `^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$` |
| `products[].translations[].title` | `string` | yes | 1–255 characters |
| `products[].translations[].url` | `string \| null` | no | format uri · ≤ 2048 characters |
| `products[].url` | `string` | yes | https on a store origin or extra_link_domains (else url_not_allowed) · format uri · ≤ 2048 characters |
| `products[].variants` | `CatalogVariantInput[]` | yes | ≥ 1 items |
| `products[].variants[].add_to_cart_url` | `string \| null` | no | format uri · ≤ 2048 characters |
| `products[].variants[].available_qty` | `integer \| null` | no |  |
| `products[].variants[].compare_at_price` | `Money \| null` | no |  |
| `products[].variants[].compare_at_price.amount` | `string` | yes | pattern `^-?\d+(\.\d{1,6})?$` |
| `products[].variants[].compare_at_price.currency` | `string` | yes | pattern `^[A-Z]{3}$` |
| `products[].variants[].external_id` | `string` | yes | 1–128 characters |
| `products[].variants[].gtin` | `string \| null` | no | ≤ 64 characters |
| `products[].variants[].image_url` | `string \| null` | no | format uri · ≤ 2048 characters |
| `products[].variants[].is_compare_at_reference` | `boolean` | no |  |
| `products[].variants[].is_purchasable` | `boolean` | no |  |
| `products[].variants[].options` | `{ [key: string]: string }` | no |  |
| `products[].variants[].price` | `Money` | yes | An amount with its currency (`03` §2.4 `Money`). |
| `products[].variants[].price.amount` | `string` | yes | pattern `^-?\d+(\.\d{1,6})?$` |
| `products[].variants[].price.currency` | `string` | yes | pattern `^[A-Z]{3}$` |
| `products[].variants[].price_includes_tax` | `boolean` | no |  |
| `products[].variants[].prices` | `CatalogPriceInput[]` | no |  |
| `products[].variants[].prices[].compare_at_price` | `Money` | no | An amount with its currency (`03` §2.4 `Money`). |
| `products[].variants[].prices[].price` | `Money` | yes | An amount with its currency (`03` §2.4 `Money`). |
| `products[].variants[].prices[].price_list` | `string` | yes | pattern `^(currency:[A-Z]{3}\|market:[A-Z]{2})$` |
| `products[].variants[].sku` | `string \| null` | no | ≤ 255 characters |
| `products[].variants[].stock_status` | `"in_stock" \| "low_stock" \| "out_of_stock" \| "backorder" \| "unknown"` | no |  |
| `products[].variants[].title` | `string \| null` | no | Default: option values joined " / " · ≤ 255 characters |
| `run_id` | `string` | no | pattern `^[A-Za-z0-9_-]{1,64}$` |

## Example request [#example-request]

```bash
curl -X POST "https://api.shopassist.coec.dk/v1/stores/sto_01j8000000e008000000000000/catalog/products:batch" \
  -H "Authorization: Bearer $SHOPASSIST_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"batch_index":0,"is_last_batch":true,"mode":"upsert","products":[{"attributes":[{"code":"string","label":"string","values":["string"]}],"brand":"string","category_external_ids":["string"],"description_html":"string","description_text":"string","external_id":"string","handle":"string","images":[{"alt":"string","url":"https://shop.example/products/linen-shirt","variant_external_ids":["string"]}],"is_published":true,"product_type":"string","status":"active","tags":["string"],"title":"string","translations":[{"description_text":"string","locale":"da-DK","title":"string","url":"https://shop.example/products/linen-shirt"}],"url":"https://shop.example/products/linen-shirt","variants":[{"add_to_cart_url":"https://shop.example/products/linen-shirt","available_qty":1,"compare_at_price":{"amount":"499.95","currency":"DKK"},"external_id":"string","gtin":"string","image_url":"https://shop.example/products/linen-shirt","is_compare_at_reference":true,"is_purchasable":true,"options":{},"price":{"amount":"499.95","currency":"DKK"},"price_includes_tax":true,"prices":[{"compare_at_price":null,"price":null,"price_list":null}],"sku":"string","stock_status":"in_stock","title":"string"}]}],"run_id":"string"}'
```

## Response 202 (BatchAccepted) [#response]

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `accepted` | `integer` | yes | ≥ 0 |
| `rejected` | `ItemRejection[]` | yes |  |
| `rejected[].error` | `ItemError` | yes | `{ code; message }` of a rejected item. |
| `rejected[].error.code` | `"validation_error" \| "currency_mismatch" \| "duplicate_external_id" \| "too_many_variants" \| "url_not_allowed"` | yes |  |
| `rejected[].error.message` | `string` | yes |  |
| `rejected[].index` | `integer` | yes | ≥ 0 |
| `sync_run_id` | `string` | yes | pattern `^sync_[0-9a-hjkmnp-tv-z]{26}$` |

```json
{
  "accepted": 0,
  "rejected": [
    {
      "error": {
        "code": "validation_error",
        "message": "string"
      },
      "index": 0
    }
  ],
  "sync_run_id": "sync_01j8000000e008000000000000"
}
```

## Errors [#errors]

| Status | Meaning |
| --- | --- |
| 400 | Bad Request ([error codes](/docs/api/errors)) |
| 401 | Unauthorized ([error codes](/docs/api/errors)) |
| 403 | Forbidden ([error codes](/docs/api/errors)) |
| 404 | Not Found ([error codes](/docs/api/errors)) |
| 409 | Conflict ([error codes](/docs/api/errors)) |
| 413 | Content Too Large ([error codes](/docs/api/errors)) |
| 422 | Unprocessable Content ([error codes](/docs/api/errors)) |
| 429 | Too Many Requests ([error codes](/docs/api/errors)) |
