# Zápis přes REST API

> Jak přes REST API zapisovat: zapnutí zápisu u klíče, Idempotency-Key, tvar odpovědi, ETag, dávky, rizikové zápisy se schválením, vrácení změn, důsledky zápisu, stropy a chyby.

- Sekce: Pro vývojáře
- Stránka: https://shopzi.cz/dokumentace/vyvojari/rest-zapis
- Ověřeno: 2026-10-29

---

REST API umí od 29. října 2026 i zapisovat. Zápisových cest je 39
a pokrývají objednávky, zásilky a vratky, katalog a ceny, sklad, zákazníky, slevové kódy a vrácení
změn. Hodí se pro ERP, skladový systém, PIM, CRM nebo pokladnu, které mají v obchodě něco měnit, ne
jen číst. 10 z nich jsou rizikové (zrušení objednávky,
faktura, cenová nabídka, dobropis, vrácení peněz, vyřízení reklamace) a provedou se až po schválení
vlastníkem nebo manažerem obchodu v administraci, viz oddíl **Rizikové zápisy se schválením**.

Každý zápis přes REST provede **tentýž nástroj** jako asistent přes MCP a Agent v administraci: se
stejnou validací, stejnými pravidly obchodu, záznamem v deníku změn a se stejnými e-maily zákazníkům
a webhooky. REST k tomu přidává jen to, co potřebuje program: striktní tělo v JSON, hlavičku
`Idempotency-Key`, ETag a dávky.

> **Zápis je opt-in na klíč** Zápisové oprávnění samo nestačí. Klíč zapisuje přes REST jen tehdy, když má vlastník obchodu u klíče zaškrtnuté „Povolit zápis přes REST API“. Klíče vydané dřív, které zápisová oprávnění mají kvůli serveru MCP, tak přes REST bez rozhodnutí vlastníka nezačnou zapisovat.

## Zapnutí zápisu [#zapnuti-zapisu]

Klíč zapisuje přes REST, když platí obojí:

1. **Má zápisové oprávnění** té oblasti, do které zapisuje (`orders:write`, `products:write`,
   `stock:write`, `customers:write`, `marketing:write`, `undo:write`, a na doklady a vrácení peněz
   `documents:write`). Zápisová oprávnění dřív platila jen pro server MCP; teď platí pro server MCP
   vždy a pro REST API, když to klíč dovolí.
2. **Má zaškrtnuté „Povolit zápis přes REST API“.**

Zaškrtávátko nastavuje vlastník obchodu v **Nastavení → API a MCP**, v kartě
**API klíče**, při vytvoření klíče (**Vytvořit klíč**) nebo u hotového klíče ikonou **Upravit klíč**:

1. Klíč musí platit pro REST API: v poli **Kde klíč platí** volba **Jen REST API**, nebo **REST API
   i server MCP**. Klíč jen pro server MCP zaškrtávátko nenabídne.
2. Ve skupině **Zápis** zaškrtněte oprávnění, která integrace potřebuje. Zaškrtávátko **Povolit zápis
   přes REST API** se ukáže až s prvním zápisovým oprávněním.
3. Zaškrtněte ho, uložte a krok potvrďte heslem nebo kódem z e-mailu. Všichni vlastníci obchodu
   dostanou e-mail a upozornění v administraci, že klíč smí zapisovat i přes REST API.

**Rychlá volba** v dialogu **Nový API klíč** nabízí sadu **ERP a sklad (REST)**: plocha jen REST
API a oprávnění ke čtení i zápisu objednávek, katalogu, skladu a zákazníků, čtení dokladů, vrácení
změn a dokumentace. Zaškrtávátko zápisu přes REST ani u ní nezaškrtne nic za vás.

Když klíči úpravou odeberete poslední zápisové oprávnění, zápis přes REST se vypne sám. Plochu
klíče úprava nezmění; klíč jen pro server MCP nahradíte novým.

### Jak to ověřit [#jak-to-overit]

`GET /api/v1/ping` vrací v poli `rest_write`, jestli klíč přes REST zapisovat smí: `true` znamená
zaškrtnutý zápis a aspoň jedno zápisové oprávnění.

```json
{
  "ok": true,
  "key_id": "9d2c1f7a-4b60-4f2e-9a11-8f0d2c3b5e64",
  "scopes": ["orders:read", "orders:write", "stock:read", "stock:write"],
  "rest_write": true,
  "api_version": "v1"
}
```

Klíč bez zaškrtávátka dostane na zápisové cestě `403 rest_write_not_enabled` a klíč bez oprávnění
`403 insufficient_scope`. `403 rest_write_disabled` znamená, že SHOPZI dočasně vypnulo zápisy přes
REST pro všechny obchody: čtení a webhooky běží dál a na klíči ani v obchodě se nic měnit nemusí.

## Pravidla požadavku [#pravidla-pozadavku]

Zápisové cesty odpovídají na `POST`, `PUT`, `PATCH` a `DELETE`. Platí pro ně:

* **Tělo je JSON** s hlavičkou `Content-Type: application/json`, jinak přijde
  `415 unsupported_media_type`. Akce bez parametrů (`DELETE`, naskladnění vratky, vrácení změny) tělo
  mít nemusí; prázdné tělo se čte jako `{}`.
* **Tělo má nejvýš 256 KB**, větší dostane `413 payload_too_large`.
* **Tělo je striktní.** Pole jsou v `snake_case` přesně podle smlouvy cesty (najdete je v referenci
  a v OpenAPI). Chybějící povinné pole, špatný typ i pole, které cesta nezná, vrátí
  `400 invalid_body` s `error.pointer`, tedy JSON Pointer na místo, které neprošlo. Překlep v názvu
  pole tak nezapadne jako „nic se nezměnilo“.
* **Žádná hlavička `Origin`.** Požadavek z webové stránky dostane `403 origin_not_allowed`. Klíč se
  zápisem patří na server integrace, nikdy do prohlížeče, kde si ho přečte kdokoli.
* **Identifikátor v cestě je UUID**, cokoli jiného vrátí `404 not_found`.
* **Částky jsou celá čísla v haléřích** (`_minor`) s DPH a sazba DPH celé procento, stejně jako
  ve čtení.

```json
{
  "error": {
    "code": "invalid_body",
    "message": "Unknown field(s) at /: quantity_chnage.",
    "pointer": ""
  }
}
```

Kontroly jdou v tomhle pořadí a odpověď rozhodne první, která neprojde:

1. hlavička `Origin` (`403 origin_not_allowed`);
2. brány klíče jako u čtení, s dvěma navíc: hned za vypnutím plochy pro všechny obchody přijde
   `403 rest_write_disabled` a za chybějícím oprávněním `403 rest_write_not_enabled`; pořadí
   popisuje článek [Autentizace a oprávnění](/dokumentace/vyvojari/autentizace-a-opravneni);
3. strop volání (`429 rate_limited`);
4. velikost a typ těla (`413`, `415`, neplatný JSON jako `400 invalid_body`);
5. identifikátor v cestě (`404 not_found`);
6. tělo proti smlouvě (`400 invalid_body`);
7. `Idempotency-Key` (`428 idempotency_key_required`, `400 invalid_idempotency_key`);
8. `If-Match` (`400 invalid_if_match`, `412 precondition_failed`);
9. u rizikové cesty to, co by zápis odmítl i po schválení (vratka jiné objednávky jako
   `404 not_found`, chybějící pole jako `422 invalid_input`, chybějící `documents:write` u zrušení
   objednávky s fakturou jako `403 forbidden`), a potom schválení (`202`, `403 approval_declined`);
10. pravidla obchodu v samotném zápisu (`409 conflict`, `422 invalid_input`, `403 forbidden` a další).

Do stropu volání se tedy počítá i zápis, který potom skončí na tělu nebo na pravidlech obchodu.

## Idempotency-Key [#idempotency-key]

Síť selhává, a program, který po výpadku zopakuje „naskladni 12 kusů“, nesmí naskladnit 24. Proto
zápisy berou hlavičku `Idempotency-Key`: hodnotu, kterou si vymyslíte pro **jednu zamýšlenou změnu**
a pošlete znovu, když tutéž změnu opakujete.

* Tvar je `^[A-Za-z0-9._:-]{8,128}$`: 8 až 128 znaků z písmen, číslic a znaků `.`,
  `_`, `:` a `-`. Jiný tvar vrátí `400 invalid_idempotency_key`. Dobře poslouží UUID nebo klíč
  odvozený z vašeho dokladu, třeba `prijemka-2026-1042-radek-3`.
* Cesty, které něco zakládají nebo hýbou zbožím, ho **vyžadují** a bez něj vrátí
  `428 idempotency_key_required`: `POST /api/v1/categories`, `POST /api/v1/credit-notes/{id}/cancel`, `POST /api/v1/customers`, `POST /api/v1/discounts`, `POST /api/v1/invoices`, `POST /api/v1/orders/{id}/cancel`, `POST /api/v1/orders/{id}/invoice`, `POST /api/v1/orders/{id}/quotation`, `POST /api/v1/orders/{id}/refunds`, `POST /api/v1/orders/{id}/reject`, `POST /api/v1/orders/{id}/returns`, `POST /api/v1/orders/{id}/shipments`, `POST /api/v1/products`, `POST /api/v1/returns/{id}/credit-note`, `POST /api/v1/returns/{id}/discount-credit-note`, `POST /api/v1/returns/{id}/reklamace-resolution`, `POST /api/v1/returns/{id}/restock` a `POST /api/v1/stock/movements`. U ostatních
  zápisů je nepovinný, ale doporučujeme ho posílat vždycky.
* **Opakování se stejným klíčem a stejným požadavkem neprovede druhý zápis.** Vrátí aktuální podobu zdroje,
  hlavičku `Idempotent-Replayed: true` a v `change.replayed` hodnotu `true`. Druhý zápis ani druhý
  webhook nevznikne.
* **Stejný klíč s jinou cestou nebo jiným tělem** je jiný požadavek a dostane
  `422 idempotency_key_reused`. Na novou změnu pošlete nový klíč.
* **Když zápis pod tím klíčem právě běží**, přijde `409 idempotency_in_progress` s hlavičkou
  `Retry-After`. Počkejte a pošlete týž požadavek znovu: dostanete jeho výsledek.
* **Když zápis selhal a nic se nezapsalo**, klíč se uvolní a opakování se stejným klíčem proběhne
  normálně. Zápis, který se přerušil uprostřed a je starší než deset minut, už se stejným klíčem
  zopakovat nejde (`409 conflict`): zkontrolujte výsledek čtením a případnou novou změnu pošlete
  s novým klíčem.

Klíč idempotence patří vašemu API klíči: dva klíče téhož obchodu se stejnou hodnotou se nesrazí.
Pamatuje se tak dlouho, jak dlouho deník drží záznam změny, tedy 30 dní (oddíl **Jak dlouho deník
změny drží** níž).

## Ukázka zápisu [#ukazka-zapisu]

Naskladnění dvanácti kusů z příjemky skladového systému:

```bash
curl -s -X POST https://muj-eshop.cz/api/v1/stock/movements \
  -H "Authorization: Bearer $SHOPZI_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: prijemka-2026-1042-radek-3" \
  -d '{
    "variant_id": "5f0b8a3e-2c71-4d9a-b6e4-0a1c3d5e7f92",
    "type": "receipt",
    "quantity_change": 12,
    "notes": "Příjemka 2026/1042"
  }'
```

Odpověď `201 Created` s hlavičkami `X-Shopzi-Change-Id` a `ETag`:

```json
{
  "data": {
    "variant_id": "5f0b8a3e-2c71-4d9a-b6e4-0a1c3d5e7f92",
    "product_id": "b1f0c0de-8f4a-4f6e-9a1c-2d3e4f5a6b70",
    "sku": "NORD-L-CER",
    "title": "Lněná košile Nord",
    "variant": "Černá / L",
    "on_hand": 16,
    "available": 16,
    "updated_at": "2026-10-28T09:14:02.118204Z",
    "created_at": "2026-08-19T11:02:41.512000Z"
  },
  "change": {
    "id": "3c9e5b1d-7a24-4f60-8e1b-6d2f0a4c8b17",
    "operation": "movement",
    "changed_fields": ["movement"],
    "replayed": false,
    "reversible": true
  }
}
```

Když síť spadne a požadavek pošlete znovu se stejným `Idempotency-Key`, přijde táž odpověď
s `"replayed": true` a hlavičkou `Idempotent-Replayed: true`; zásoba zůstane na 16.

## Tvar odpovědi [#tvar-odpovedi]

Úspěšný zápis vrací `200`, u založení `201`, a tělo `{ "data": …, "change": … }`:

* `data` je zdroj **v témže tvaru, jaký vrací čtení**: objednávka jako `GET /api/v1/orders/{id}`,
  produkt jako `GET /api/v1/products/{id}`, zásilka, vratka, doklad, kategorie, zákazník, slevový
  kód nebo stav skladu varianty jako v příslušném výpisu. Zápis do varianty vrací celý produkt.
  Dávky vracejí výsledek po položkách (oddíl **Dávky**); `DELETE` a storno dobropisu vracejí
  `data: null`.
* `change` popisuje záznam v deníku změn: `id` (tentýž jako v hlavičce `X-Shopzi-Change-Id`),
  `operation`, `changed_fields` (pole, která zápis opravdu změnil), `replayed` a `reversible`.
  `reversible` je `false` u cest, které vrátit nejde, a u zápisu, který nic nezměnil.

Ve vzácném případě, kdy se zápis provede, ale zdroj se hned potom nedá přečíst (mezitím ho někdo
smazal), přijde `404 not_found` a hlavička `X-Shopzi-Change-Id` nese id změny, která se zapsala.
Odmítnutí nástroje nese v `message` českou větu, kterou můžete ukázat obchodníkovi; rozhodujte se
ale podle `code`.

## ETag a If-Match [#etag-a-if-match]

Detail zdroje (`GET /api/v1/orders/{id}`, `/api/v1/products/{id}`, `/api/v1/customers/{id}`,
`/api/v1/discounts/{id}`, `/api/v1/returns/{id}`, `/api/v1/documents/{id}`) i každý zápis, který
vrací zdroj, nesou hlavičku `ETag` ve tvaru `W/"…"`: slabý otisk (SHA-256) zdroje tak, jak ho
odpověď vrací.

Pošlete-li ho u zápisu v hlavičce `If-Match`, zápis proběhne jen tehdy, když zdroj pořád vypadá tak,
jak jste ho četli; jinak přijde `412 precondition_failed` a nic se nezmění. `If-Match: *` znamená
„cokoli, co existuje“. Hodnota v jiném tvaru vrátí `400 invalid_if_match`. U založení se `If-Match`
nepoužije; zásilka a vratka, které se zakládají k objednávce, porovnávají ETag objednávky.

> **If-Match je best effort** Porovnání a zápis nedělí zámek. Podmínka vás ochrání před rozhodnutím nad zastaralým stavem (ERP přecení produkt, který obchodník mezitím upravil), ne před dvěma zápisy v téže milisekundě. Ty hlídají pravidla zápisu a deník změn.

## Dávky [#davky]

Dvě cesty zapisují až 100 položek jedním požadavkem:
`PUT /api/v1/prices/batch` a `POST /api/v1/stock/levels/batch`. Každá varianta smí být v dávce jen jednou.

* `PUT /api/v1/prices/batch` přecení varianty, každou vlastní cenou. **Platí celá dávka, nebo nic.**
  Zapíše se jeden záznam v deníku, za každý dotčený produkt přijde jedna událost `product.updated`
  a celou dávku vrátí jedno vrácení změny. Každá změna ceny se zapíše do 30denní historie cen
  podle § 12a zákona o ochraně spotřebitele, stejně jako přecenění v administraci.
* `POST /api/v1/stock/levels/batch` nastaví stav skladu variant na čísla, jak je hlásí váš sklad, a
  vrátí výsledek **po položkách**: `applied` (zapsala se korekce), `unchanged` (stav už seděl),
  `refused` (varianta by šla do záporu) a `not_found` (varianta v obchodě není). Probíhá v jedné
  transakci a do deníku se zapíše jedním záznamem, takže se i vrací jedním krokem.

**Dávka se do stropu volání počítá jako jeden zápis**, ať nese jednu položku, nebo sto. Sto
jednotlivých volání by spotřebovalo sto zápisů a sto záznamů v deníku.

## Vrácení změny a deník změn [#vraceni-zmeny-a-denik-zmen]

`GET /api/v1/changes` vypisuje deník změn obchodu: co se změnilo přes REST, asistenta přes MCP
nebo Agenta, kdy, kterým klíčem (`api_key_id`), odkud (`source`: `api`, `mcp`, `agent`, `system`)
a jestli je změna vrácená. Jsou to **jen metadata**; hodnoty před změnou a po ní REST nevydává.
Výpis se stránkuje a synchronizuje jako ostatní, filtrovat jde podle `source` a `status`.

`POST /api/v1/changes/{id}/revert` vrátí jednu změnu, ať ji zapsal REST, asistent nebo Agent. Chce
oprávnění `undo:write` **a** zápisové oprávnění oblasti, které se změna týká (vrácení ceny chce
i `products:write`). Když položku od té změny někdo upravil, vrácení se odmítne, aby nepřepsalo
novější práci. Sklad se nevrací přepsáním čísla, ale vyrovnávacím pohybem, takže historie pohybů
dál sedí. Druhé vrácení téže změny nic neudělá.

Vrátit **nejde** tyhle zápisy: `POST /api/v1/credit-notes/{id}/cancel`, `POST /api/v1/customers`, `POST /api/v1/invoices`, `POST /api/v1/orders/{id}/cancel`, `POST /api/v1/orders/{id}/invoice`, `POST /api/v1/orders/{id}/payment-status`, `POST /api/v1/orders/{id}/quotation`, `POST /api/v1/orders/{id}/refunds`, `POST /api/v1/orders/{id}/reject`, `POST /api/v1/orders/{id}/returns`, `POST /api/v1/orders/{id}/shipments`, `POST /api/v1/orders/{id}/status`, `POST /api/v1/returns/{id}/approval`, `POST /api/v1/returns/{id}/credit-note`, `POST /api/v1/returns/{id}/discount-credit-note`, `POST /api/v1/returns/{id}/reklamace-resolution`, `POST /api/v1/returns/{id}/restock`, `POST /api/v1/returns/{id}/status`, `POST /api/v1/shipments/{id}/status` a `PUT /api/v1/shipments/{id}/tracking`. Každý z nich
má důsledek, který vrácení řádku v deníku nevezme zpět:

* **Stav objednávky a stav platby** posílají zákazníkovi e-mail a zaplacení může vystavit fakturu
  s číslem z řady dokladů. Peníze se vracejí dobropisem a refundací k vratce, obojí jen po schválení
  (oddíl **Rizikové zápisy se schválením**).
* **Zásilky** (založení, stav i sledování) žijí zároveň u dopravce: svoz se skutečně objedná,
  předání pošle zákazníkovi e-mail a zrušení jde i k dopravci. Vrácení řádku v obchodě by to
  nevzalo zpět.
* **Vratky a reklamace** dostávají číslo z řady, u reklamace běží zákonná lhůta a naskladnění
  vratky je skutečný skladový pohyb. Chybu opraví další zápis, ne vrácení.
* **Založení zákazníka** se nevrací: účet se ruší výmazem podle GDPR, který schvaluje člověk.

## Co zápis spustí [#co-zapis-spusti]

Zápis přes REST má tytéž důsledky jako totéž v administraci:

| Cesta                                                                                     | Co se stane navíc                                                                                                                                                                                                                                                               |
| ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `POST /api/v1/orders/{id}/status`                                                         | Zákazník dostane e-mail o stavu jako z administrace. S `"notify_customer": false` se stav zapíše bez e-mailu (dosynchronizování historie). Stav jde jen dopředu; zrušení a zamítnutí jsou vlastní cesty se schválením.                                                          |
| `POST /api/v1/orders/{id}/payment-status`                                                 | `paid` pošle zákazníkovi potvrzení platby, zapíše tržbu a podle nastavení obchodu vystaví fakturu. Nechce žádné schválení. `refunded` a `partially_refunded` tahle cesta odmítne odpovědí `403 approval_required`: peníze se vracejí cestou `POST /api/v1/orders/{id}/refunds`. |
| `POST /api/v1/orders/{id}/shipments`                                                      | Bez pole `external` objedná u dopravce s napojením **skutečný svoz**. S `external` (`tracking_number`, `tracking_url`) jen zapíše zásilku, kterou jste podali sami, a dopravci nic neposílá.                                                                                    |
| `POST /api/v1/shipments/{id}/status`                                                      | Předání dopravci pošle zákazníkovi e-mail, doručení dobírky označí platbu a vystaví doklad, zrušení zruší zásilku i u dopravce.                                                                                                                                                 |
| `POST /api/v1/orders/{id}/returns`                                                        | Vratka dostane číslo z řady, u reklamace se rozběhne zákonná lhůta.                                                                                                                                                                                                             |
| `POST /api/v1/returns/{id}/restock`                                                       | Zapíše skladový pohyb za schválené kusy.                                                                                                                                                                                                                                        |
| `PUT /api/v1/variants/{id}/price` a `PUT /api/v1/prices/batch`                            | Cena se zapíše do 30denní historie cen podle § 12a.                                                                                                                                                                                                                             |
| `POST /api/v1/orders/{id}/cancel`                                                         | Po schválení: zrušení tímtéž řetězcem jako v administraci. Zastaví zásilky u dopravce a k vystavené faktuře vystaví storno dobropis (to chce i `documents:write`). Zákazník dostane e-mail.                                                                                     |
| `POST /api/v1/orders/{id}/reject`                                                         | Po schválení: zamítnutí objednávky, zákazník dostane e-mail. Dobropis nevzniká.                                                                                                                                                                                                 |
| `POST /api/v1/orders/{id}/invoice`, `POST /api/v1/invoices`                               | Po schválení: faktura s číslem z bezmezerové řady, s `"send_email": true` hned e-mailem zákazníkovi.                                                                                                                                                                            |
| `POST /api/v1/orders/{id}/quotation`                                                      | Po schválení: cenová nabídka s číslem z řady k naceněné poptávce. Zákazníkovi se sama neposílá.                                                                                                                                                                                 |
| `POST /api/v1/returns/{id}/credit-note`, `POST /api/v1/returns/{id}/discount-credit-note` | Po schválení: dobropis s číslem z řady. Peníze se tím nevracejí.                                                                                                                                                                                                                |
| `POST /api/v1/credit-notes/{id}/cancel`                                                   | Po schválení: storno dobropisu, jeho číslo zůstane v řadě jako stornované. Jen dokud se proti vratce nehnuly peníze.                                                                                                                                                            |
| `POST /api/v1/orders/{id}/refunds`                                                        | Po schválení: `gateway` vrátí peníze přes platební bránu, `manual` zapíše peníze vrácené převodem nebo hotově, `shipping` nastaví vrácení za dopravu. Zákazník dostane e-mail o vrácení peněz.                                                                                  |
| `POST /api/v1/returns/{id}/reklamace-resolution`                                          | Po schválení: vyřízení reklamace podle § 2169 OZ s příznakem zákonné lhůty. Zákazník dostane zprávu.                                                                                                                                                                            |

Webhooky vznikají stejně jako u změny v administraci; které událost která cesta vypouští, uvádí
reference a OpenAPI. Opakování se stejným `Idempotency-Key` žádnou událost nepošle.

Zápis přes REST API **nestojí kredity**: žádná zápisová cesta nespouští AI a měsíční strop kreditů
klíče se REST API netýká.

## Zápisové cesty [#zapisove-cesty]

| Cesta                                            | Oprávnění         | Idempotency-Key | Jde vrátit | Schválení |
| ------------------------------------------------ | ----------------- | --------------- | ---------- | --------- |
| `POST /api/v1/orders/{id}/status`                | `orders:write`    | nepovinný       | ne         | ne        |
| `POST /api/v1/orders/{id}/payment-status`        | `orders:write`    | nepovinný       | ne         | ne        |
| `POST /api/v1/orders/{id}/shipments`             | `orders:write`    | povinný         | ne         | ne        |
| `PUT /api/v1/orders/{id}/charges`                | `orders:write`    | nepovinný       | ano        | ne        |
| `POST /api/v1/orders/{id}/returns`               | `orders:write`    | povinný         | ne         | ne        |
| `POST /api/v1/shipments/{id}/status`             | `orders:write`    | nepovinný       | ne         | ne        |
| `PUT /api/v1/shipments/{id}/tracking`            | `orders:write`    | nepovinný       | ne         | ne        |
| `POST /api/v1/returns/{id}/status`               | `orders:write`    | nepovinný       | ne         | ne        |
| `POST /api/v1/returns/{id}/approval`             | `orders:write`    | nepovinný       | ne         | ne        |
| `POST /api/v1/returns/{id}/restock`              | `orders:write`    | povinný         | ne         | ne        |
| `POST /api/v1/documents/{id}/paid`               | `orders:write`    | nepovinný       | ano        | ne        |
| `POST /api/v1/products`                          | `products:write`  | povinný         | ano        | ne        |
| `PATCH /api/v1/products/{id}`                    | `products:write`  | nepovinný       | ano        | ne        |
| `PUT /api/v1/products/{id}/categories`           | `products:write`  | nepovinný       | ano        | ne        |
| `PATCH /api/v1/variants/{id}`                    | `products:write`  | nepovinný       | ano        | ne        |
| `PUT /api/v1/variants/{id}/price`                | `products:write`  | nepovinný       | ano        | ne        |
| `PUT /api/v1/prices/batch`                       | `products:write`  | nepovinný       | ano        | ne        |
| `POST /api/v1/categories`                        | `products:write`  | povinný         | ano        | ne        |
| `PATCH /api/v1/categories/{id}`                  | `products:write`  | nepovinný       | ano        | ne        |
| `POST /api/v1/stock/movements`                   | `stock:write`     | povinný         | ano        | ne        |
| `PUT /api/v1/stock/{variant_id}`                 | `stock:write`     | nepovinný       | ano        | ne        |
| `POST /api/v1/stock/levels/batch`                | `stock:write`     | nepovinný       | ano        | ne        |
| `POST /api/v1/customers`                         | `customers:write` | povinný         | ne         | ne        |
| `PATCH /api/v1/customers/{id}`                   | `customers:write` | nepovinný       | ano        | ne        |
| `PUT /api/v1/customers/{id}/tags`                | `customers:write` | nepovinný       | ano        | ne        |
| `POST /api/v1/discounts`                         | `marketing:write` | povinný         | ano        | ne        |
| `PATCH /api/v1/discounts/{id}`                   | `marketing:write` | nepovinný       | ano        | ne        |
| `DELETE /api/v1/discounts/{id}`                  | `marketing:write` | nepovinný       | ano        | ne        |
| `POST /api/v1/changes/{id}/revert`               | `undo:write`      | nepovinný       | ne         | ne        |
| `POST /api/v1/orders/{id}/cancel`                | `orders:write`    | povinný         | ne         | ano       |
| `POST /api/v1/orders/{id}/reject`                | `orders:write`    | povinný         | ne         | ano       |
| `POST /api/v1/orders/{id}/refunds`               | `documents:write` | povinný         | ne         | ano       |
| `POST /api/v1/orders/{id}/invoice`               | `documents:write` | povinný         | ne         | ano       |
| `POST /api/v1/orders/{id}/quotation`             | `documents:write` | povinný         | ne         | ano       |
| `POST /api/v1/invoices`                          | `documents:write` | povinný         | ne         | ano       |
| `POST /api/v1/returns/{id}/credit-note`          | `documents:write` | povinný         | ne         | ano       |
| `POST /api/v1/returns/{id}/discount-credit-note` | `documents:write` | povinný         | ne         | ano       |
| `POST /api/v1/credit-notes/{id}/cancel`          | `documents:write` | povinný         | ne         | ano       |
| `POST /api/v1/returns/{id}/reklamace-resolution` | `documents:write` | povinný         | ne         | ano       |

Několik pravidel, na která integrace narazí nejčastěji:

* `PATCH /api/v1/products/{id}` mění texty, stav, značku a SEO; `status: archived` stáhne produkt
  z prodeje a data zůstanou. Adresu produktu (URL) REST nemění a cenu mění cesta varianty.
* `PUT /api/v1/stock/{variant_id}` nastaví **celkový** stav varianty přes sklady. Rozdíl se spočítá
  pod zámkem a zapíše jako korekce na výchozím skladu, takže kniha pohybů dál sedí.
* `PATCH /api/v1/customers/{id}` mění jméno, telefon, firmu, IČO a DIČ. E-mail ne: je to
  přihlašovací údaj zákazníka. Vystavené doklady si nesou údaje z okamžiku vystavení.
* `DELETE /api/v1/discounts/{id}` smaže jen kód, který nikdo neuplatnil. Uplatněný kód archivujte
  (`PATCH` se `status: archived`): záznamy o použití jsou účetní podklad.
* Kategorie se zakládají nejvýš do třetí úrovně stromu; adresu a místo kategorie ve stromu REST
  nemění.

Pole těla, jejich typy a chyby každé cesty vypisuje reference generovaná ze smlouvy (například
[REST API: objednávky, vratky a zásilky](/dokumentace/vyvojari/rest-objednavky)) a strojově
[OpenAPI popis REST API](/dokumentace/vyvojari/openapi).

## Rizikové zápisy se schválením [#rizikove-zapisy-se-schvalenim]

Zrušení a zamítnutí objednávky, faktura k objednávce i samostatná, cenová nabídka, dobropis a jeho
storno, vrácení peněz a vyřízení reklamace jsou nevratné: doklad je neměnný a jeho číslo z řady se
znovu nepoužije, peníze jsou u zákazníka a e-mail mu nikdo nevezme zpátky. Proto je každou z těch
cest (`POST /api/v1/credit-notes/{id}/cancel`, `POST /api/v1/invoices`, `POST /api/v1/orders/{id}/cancel`, `POST /api/v1/orders/{id}/invoice`, `POST /api/v1/orders/{id}/quotation`, `POST /api/v1/orders/{id}/refunds`, `POST /api/v1/orders/{id}/reject`, `POST /api/v1/returns/{id}/credit-note`, `POST /api/v1/returns/{id}/discount-credit-note` a `POST /api/v1/returns/{id}/reklamace-resolution`) potřeba napřed **schválit v administraci obchodu**,
stejně jako tytéž akce přes asistenta MCP.

1. Pošlete požadavek s hlavičkou `Idempotency-Key` (u rizikových cest je povinná). Odpověď je
   `202 Accepted`, **nic se neprovede** a tělo nese schválení:

   ```json
   {
     "approval": {
       "id": "8c1e4f2a-3b5d-4c6e-9f70-1a2b3c4d5e6f",
       "status": "pending",
       "approval_url": "https://muj-eshop.cz/administrace/schvaleni/8c1e4f2a-3b5d-4c6e-9f70-1a2b3c4d5e6f",
       "expires_at": "2026-10-28T10:15:00.000000Z"
     }
   }
   ```

   Hlavička `Location` vede na `GET /api/v1/approvals/{id}` a `Retry-After` říká, za kolik sekund
   se zeptat znovu. Vlastníci a manažeři obchodu dostanou upozornění v administraci.
2. Odkaz `approval_url` otevře **vlastník nebo manažer** přihlášený do administrace a změnu schválí,
   nebo zamítne. Editor, účetní ani účet podpory SHOPZI schválit nemůžou. Odkaz nenese žádné
   přihlášení, takže ho klidně pošlete e-mailem.
3. Stav sledujte přes `GET /api/v1/approvals/{id}` (`pending`, `approved`, `consumed`, `declined`,
   `expired`), nebo počkejte na webhook `approval.decided`. Stav schválení vidí jen klíč, který o něj
   požádal; jinému klíči vrátí `404 not_found`.
4. Po schválení pošlete **týž požadavek se stejným `Idempotency-Key`**. Zápis proběhne právě jednou
   a odpověď je jako u každého jiného zápisu (`200`, u vystavení dokladu `201`, s `data` a `change`).
   Doklad přijde v témže tvaru jako řádek `GET /api/v1/documents`, i s číslem z řady.

Co dál platí:

* **Opakování po zápisu vrátí týž výsledek.** Když se vám ztratí odpověď, pošlete týž požadavek
  znovu: dostanete tentýž doklad s týmž číslem, `Idempotent-Replayed: true` a `change.replayed:
  true`. Druhý doklad ani druhá refundace nevznikne.
* **Schválení patří k jednomu `Idempotency-Key`.** Dvě refundace po sto korunách k téže vratce jsou
  dvě změny, každá s vlastním klíčem a vlastním schválením. Nový klíč se stejným tělem tedy založí
  nové schválení, neprovede to staré.
* **Zamítnuté schválení** vrátí na týž požadavek `403 approval_declined` a nic se neprovede. Kdo
  změnu pořád chce, pošle nový požadavek s novým klíčem.
* **Schválení platí hodinu.** Nerozhodnuté, nebo schválené a nepoužité schválení po hodině vyprší
  a týž požadavek založí nové (`202`).
* **Na jeden klíč** čeká nejvýš dvacet schválení naráz a za hodinu jich vznikne nejvýš třicet; nad to
  přijde `409 conflict` a nic nevznikne.
* **Co by zápis odmítl i po schválení, se odmítne dřív**, aby vlastník neschvaloval změnu, která
  stejně neprojde: vratka jiné objednávky (`404`), ruční refundace bez `method` (`422`), zrušení
  objednávky s fakturou klíčem bez `documents:write` (`403 forbidden`, storno dobropis je doklad).
* Rizikové zápisy se nevracejí přes `POST /changes/{id}/revert`. Fakturu opraví dobropis,
  dobropis jeho storno, oboje zase se schválením.

## Co přes REST nejde [#co-pres-rest-nejde]

Rozeslání kampaně, smazání přesměrování a vyřízení nedodatelné části objednávky u dodavatele umí jen
asistent přes MCP (po schválení v prohlížeči) a administrace. Feedy dodavatelů se přes REST jen
čtou a zapisují se přes server MCP nebo v administraci, viz
[REST API: feedy dodavatelů](/dokumentace/vyvojari/rest-feedy-dodavatelu).

Export objednávek, balíčku dokladů pro účetní, katalogu a podkladu k DPH do souboru zápisem není:
`POST /api/v1/exports` je čtení a chce jen čtecí oprávnění té oblasti, viz
[Endpointy REST API](/dokumentace/vyvojari/rest-api-endpointy).

## Stropy [#stropy]

Zápis je i volání REST API, takže se počítá **do obou stropů**: do stropu volání (článek
[Chybové odpovědi](/dokumentace/vyvojari/chybove-odpovedi), oddíl **Objem požadavků**) a navíc do
stropu zápisů. Oba se počítají na klíč podle tarifu obchodu, v pevném minutovém okně a za
kalendářní den v českém čase, a rozhoduje ten, který dojde dřív:

| Tarif                            | Zápisů za minutu | Zápisů za den |
| -------------------------------- | ---------------- | ------------- |
| Basic                            | 30               | 5 000         |
| Standard                         | 60               | 15 000        |
| Business                         | 120              | 40 000        |
| Enterprise a jednorázová licence | 150              | 80 000        |

Nad strop přijde `429 rate_limited` s hlavičkou `Retry-After`; hlavičky `X-RateLimit-*` popisují
okno, které je právě těsnější. Dávka je jeden zápis. Ochranná vrstva sítě navíc pustí z jedné IP
adresy zhruba 60 zápisů za minutu, a to ještě před aplikací; takové odmítnutí nemá tvar chyby API.
Tisíce změn proto posílejte dávkami, ne jednotlivě.

## Chyby [#chyby]

Všechny kódy zápisu i to, co s nimi dělat, vypisuje tabulka v článku
[Chybové odpovědi](/dokumentace/vyvojari/chybove-odpovedi). Nejčastěji:

* `400 invalid_body`: tělo neprošlo smlouvou, `error.pointer` ukazuje kam;
* `409 conflict`: zápis odporuje stavu obchodu (stav jde jen dopředu, nebo by zápis porušil
  pravidlo obchodu); stejné volání znovu neprojde;
* `422 invalid_input`: hodnotu obchod nepřijme (neexistující kategorie, odkaz bez `https`);
* `412 precondition_failed`: zdroj se od vašeho čtení změnil;
* `403 approval_declined`: rizikový zápis vlastník nebo manažer zamítl (oddíl **Rizikové zápisy se
  schválením**);
* `428 idempotency_key_required`, `422 idempotency_key_reused`, `409 idempotency_in_progress`:
  oddíl **Idempotency-Key** výš.

## Jak dlouho deník změny drží [#jak-dlouho-denik-zmeny-drzi]

Deník změn drží zápisy přes REST API **30 dní**, změny asistenta přes MCP a Agenta 365 dní. Starší
záznamy maže pravidelný úklid po dávkách. Potom změna z `GET /api/v1/changes` zmizí, vrátit ji už
nejde a zapomene se i její `Idempotency-Key`. Integrace, která potřebuje delší historii, si ji
vede u sebe.

Když obchodník vymaže zákazníka podle GDPR, smažou se i snímky jeho karty v deníku změn (jméno,
telefon, firma). Úprava takového zákazníka pak vrátit nejde, protože není co vrátit.
