# Catalog Push API

> Push products, categories, locations, stock and pages from your server in idempotent batches, including full replace_all runs.

Source: https://shopassist.coec.dk/docs/install/generic/catalog-push-api · Updated 2026-10-01

The Catalog Push API lets your server send catalog changes as they happen. It is available on **every plan**.

## Authentication [#auth]

Create an API key in **Developers → API keys** with the scope `catalog:write`, restricted to the store. Send it as `Authorization: Bearer sa_live_…` from your server only; never put it in a browser. See [authentication](/docs/api/authentication).

## Endpoints [#endpoints]

`POST /v1/stores/{store_id}/catalog/products:batch` (scope `catalog:write`)

`POST /v1/stores/{store_id}/catalog/products:delete` (scope `catalog:write`)

`POST /v1/stores/{store_id}/catalog/categories:batch` (scope `catalog:write`)

`POST /v1/stores/{store_id}/catalog/locations:batch` (scope `catalog:write`)

`POST /v1/stores/{store_id}/catalog/inventory:batch` (scope `catalog:write`)

`POST /v1/stores/{store_id}/catalog/pages:batch` (scope `catalog:write`)

Field reference: [API reference → Catalog Push API](/docs/api/reference/catalog/push-products). The `locations:batch` and `inventory:batch` endpoints also work for Magento, WooCommerce and PrestaShop stores that send store stock by feed.

## Batches [#batches]

* At most 500 items per batch and 5 MB per request; 120 batches per minute per store.
* Every batch needs an `Idempotency-Key` header. Retrying with the same key returns the original response; reusing a key with a different body fails with `422 idempotency_key_reused`. Generate a new key for each new batch and reuse it only for retries of that batch.
* The answer is `202 Accepted` with `accepted`, `rejected` (index and error per invalid item) and a `sync_run_id`. Valid items are processed even when others are rejected.
* Batches of one store are applied in the order they were received.

```bash
curl -X POST "https://api.shopassist.coec.dk/v1/stores/sto_.../catalog/products:batch" \
  -H "Authorization: Bearer $SHOPASSIST_API_KEY" \
  -H "Idempotency-Key: 7f9c3a12-batch-0001" \
  -H "Content-Type: application/json" \
  -d '{"products":[{"external_id":"SHIRT-42","title":"Linen shirt","url":"https://shop.example.com/products/linen-shirt",
       "variants":[{"external_id":"SHIRT-42-BLUE","sku":"SHIRT-42-BLUE","price":{"amount":"499.00","currency":"DKK"},"available_qty":12}]}]}'
```

Prices are decimal strings in the store currency. Product URLs must be HTTPS on one of your allowed domains (`url_not_allowed` otherwise).

## Full replace runs [#replace-all]

To replace the whole catalog (products you no longer send are removed), push all products as one run:

* every batch carries `mode: "replace_all"`, the same `run_id` (up to 64 characters, `A–Z a–z 0–9 _ -`) and a `batch_index` starting at 0;
* the last batch carries `is_last_batch: true`.

When all batches up to the last index have arrived, ShopAssist removes products that were not in the run. If that would remove more than half of the catalog, it stops and asks for confirmation in the dashboard. A run that is incomplete six hours after its first batch fails and removes nothing (`push_run_incomplete`). A batch for a finished run is refused (`409 conflict`, reason `run_closed`).

## Errors [#errors]

| Response                              | Meaning                                                                                                                           |
| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `202` with `rejected[]`               | Some items were invalid: `validation_error`, `currency_mismatch`, `duplicate_external_id`, `too_many_variants`, `url_not_allowed` |
| `409 conflict` (`feed_authoritative`) | A full primary feed is enabled for this store                                                                                     |
| `409 conflict` (`run_closed`)         | The `replace_all` run already finished                                                                                            |
| `403 forbidden` (`test_key`)          | Test keys cannot write                                                                                                            |
| `429 rate_limited`                    | Wait for `Retry-After` seconds                                                                                                    |

All error codes: [API errors](/docs/api/errors).

## Order reports [#order-reports]

To attribute revenue to conversations, report placed orders (no personal data, no order status) with the `orders:report` scope:

`POST /v1/stores/{store_id}/orders:report` (scope `orders:report`)

See [assisted revenue and attribution](/docs/analytics/assisted-revenue-and-attribution).
