# Chybové odpovědi

> Tvar chyby na REST API i na MCP, seznam kódů i s tím, kdy nastanou, a co má klient udělat s každým z nich. Kódy se generují z kódu, který je vrací.

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

---

Chyba REST API má vždycky stejný tvar a `code` je to, na co se má váš program rozhodovat. Text
v `message` je pro člověka, který se dívá do protokolu, a smí se kdykoli změnit. MCP server mluví
JSON-RPC, takže má tvar vlastní a je popsaný v samostatném oddíle níž.

```json
{
  "error": {
    "code": "insufficient_scope",
    "message": "This key is missing the `orders:read` scope. It has: products:read."
  }
}
```

> **Většina zpráv je anglicky, brány přístupu česky** Tělo chyby čte stroj a vývojář, proto je text u neplatného klíče, chybějícího oprávnění nebo neexistujícího záznamu anglicky. Výjimkou jsou odmítnutí, jejichž nápravu dělá obchodník, ne váš kód: plocha není zapnutá, obchod nemá placený tarif, přístup je pozastavený nebo dočasně vypnutý, nebo klíč patří k jiné ploše. Ta jsou česky, abyste je mohli obchodníkovi rovnou přeposlat. Rozhodujte se vždy podle `code`, ne podle textu.

## Kódy [#kody]

| Stav | Kód | Kdy nastane | Kde |
| --- | --- | --- | --- |
| 401 | `invalid_api_key` | Klíč neznáme, byl odvolaný, nebo mu vypršela platnost. Která z těch tří možností to je, schválně neprozrazujeme. | všechny cesty |
| 401 | `missing_api_key` | Hlavička s klíčem chybí, nebo hodnota nezačíná prefixem klíče. | všechny cesty |
| 403 | `external_access_disabled` | SHOPZI tuhle plochu (REST API nebo server MCP) dočasně vypnulo pro všechny obchody. Na obchodu ani na klíči nic neměňte a zkuste to později. | všechny cesty |
| 403 | `external_access_not_enabled` | Vlastník obchodu plochu nezapnul. Zapíná ji v Nastavení → API a MCP přepínačem REST API nebo Server MCP. | všechny cesty |
| 403 | `external_access_suspended` | SHOPZI externí přístup k tomuto obchodu pozastavilo. Důvod sdělí vlastníkovi obchodu podpora SHOPZI. | všechny cesty |
| 403 | `insufficient_scope` | Klíč je platný, ale nemá oprávnění, které cesta vyžaduje. Zpráva vypíše, co klíči chybí a co má. | všechny cesty |
| 403 | `key_surface_mismatch` | Klíč je vydaný jen pro druhou plochu (jen pro REST API, nebo jen pro MCP). Vlastník obchodu vydá klíč pro správnou plochu v Nastavení → API a MCP. | všechny cesty |
| 403 | `plan_required` | REST API i server MCP patří k placeným tarifům a obchod teď aktivní placený tarif nemá. Po obnovení tarifu se přístup otevře sám. | všechny cesty |
| 403 | `shop_unavailable` | Obchod je deaktivovaný nebo pozastavený, takže požadavky přes API ani MCP neobsluhuje. Klíč s tím nic nezmůže; otevře ho až obnovení obchodu. | všechny cesty |
| 404 | `not_found` | Záznam s tímhle identifikátorem v obchodě není. | všechny cesty |
| 404 | `shop_not_found` | Na doméně, na kterou požadavek přišel, žádný obchod neběží. API se volá na doméně obchodu. | všechny cesty |
| 429 | `rate_limited` | Klíč vyčerpal strop volání za minutu nebo za den podle tarifu obchodu. Počkejte tolik sekund, kolik říká hlavička Retry-After, a pokračujte. | všechny cesty |
| 500 | `server_error` | Chyba na naší straně. Opakujte s odstupem. | všechny cesty |
| 400 | `invalid_cursor` | Kurzor je poškozený, nebo patří k jinému dotazu (jiná cesta, jiný filtr nebo jiné updated_since). Začněte průchod znovu bez cursor. | všechny výpisy |
| 400 | `invalid_query` | Parametr dotazu má neplatnou hodnotu: čas v updated_since bez časového posunu, nebo hodnota filtru, kterou výčet nezná. Zpráva jmenuje parametr, u filtru i povolené hodnoty. | všechny výpisy |
| 402 | `plan_required` | Tarif obchodu feedy dodavatelů nezahrnuje, takže katalog dodavatelů se neotevře. Otevře ho tarif Basic a vyšší; ostatní cesty feedů vracejí, co obchod už má uložené. | `GET /api/v1/catalog-items` |
| 403 | `inventory_disabled` | Obchod má vypnutý modul Sklad, takže skladová data neotevírá nikomu. | `GET /api/v1/stock` `GET /api/v1/stock/movements` |
| 422 | `isdoc_invalid` | Doklad by neprošel pravidly ISDOC, typicky dobropis bez čísla opravovaného dokladu. Zpráva říká proč; opakování nepomůže, doklad je potřeba opravit v administraci. | `GET /api/v1/documents/{id}/isdoc` |
| 422 | `unsupported_document_type` | Doklad existuje, ale daňový není: objednávka, cenová nabídka ani účtenka nemají rozpis DPH, PDF faktury ani ISDOC. | `GET /api/v1/documents/{id}` `GET /api/v1/documents/{id}/isdoc` `GET /api/v1/documents/{id}/pdf` |
| 502 | `pdf_unavailable` | PDF se teď nepodařilo vyrobit. Nic se nezměnilo, zkuste to s odstupem znovu. | `GET /api/v1/documents/{id}/pdf` |
| 503 | `pdf_busy` | Obchod právě vyrábí jiná PDF a další vykreslení by čekalo ve frontě. Počkejte tolik sekund, kolik říká hlavička Retry-After, a zkuste to znovu. Doklad, jehož PDF už existuje, tuhle odpověď nedostane. | `GET /api/v1/documents/{id}/pdf` |

Kódů sdílených všemi cestami je 13. V jakém pořadí se kontroly
přístupu vyhodnocují, popisuje článek [Autentizace a oprávnění](/dokumentace/vyvojari/autentizace-a-opravneni).

## Co s nimi dělat [#co-s-nimi-delat]

* **400** znamená, že cesta dotaz nepřečetla. `invalid_query` přijde u neplatné hodnoty parametru
  (čas bez časového posunu v `updated_since`, hodnota filtru mimo výčet) a zpráva jmenuje parametr.
  `invalid_cursor` přijde u kurzoru, který se cestou poškodil nebo patří k jinému dotazu, třeba
  s jiným filtrem. Stejný dotaz znovu neprojde; u `invalid_cursor` začněte průchod bez `cursor`.
* **401** znamená, že klíč není platný nebo hlavička chybí. `missing_api_key` je požadavek úplně bez
  klíče (nebo s hodnotou, která nezačíná prefixem `shopzi_sk_`). Rozhoduje se
  o něm dřív než o přepínači plochy, takže přijde i na obchodě, který externí přístup zapnutý nemá;
  předběhne ho jen neexistující nebo pozastavený obchod a plocha dočasně vypnutá pro všechny obchody.
  `invalid_api_key` znamená klíč, který se nepodařilo ověřit. Neopakujte: znovu poslaný stejný klíč
  bude neplatný pořád. Odpověď nese hlavičku `www-authenticate`, takže na ni umí zareagovat i
  obecný HTTP klient.
* **403** znamená, že klíč je v pořádku, ale nedosáhne tam, kam sáhl. Buď mu chybí oprávnění (pak
  ho vlastník obchodu klíči doplní v **Nastavení → API a MCP** ikonou **Upravit
  klíč**), nebo je vypnutá celá funkce obchodu, jako u `inventory_disabled`. Ani jedno neopravíte
  opakováním.
  * Čtyři kódy se týkají celé plochy, na kterou voláte, ne vašeho klíče. Přicházejí **dřív, než se
    ověří platnost klíče**, takže je dostanete i se správným klíčem a naopak z nich nepoznáte,
    jestli je klíč platný. Kontroluje se to v tomhle pořadí:
    * `external_access_disabled`: plocha je na SHOPZI dočasně vypnutá pro všechny obchody (údržba
      nebo zásah při incidentu). Zkuste to později.
    * `external_access_not_enabled`: obchodu tahle plocha není zapnutá. Obchodník ji zapíná
      v administraci, viz [Autentizace a oprávnění](/dokumentace/vyvojari/autentizace-a-opravneni).
    * `external_access_suspended`: SHOPZI externí přístup tomuto obchodu pozastavil. Obchodník se
      důvod dozví od podpory; opakování ani nový klíč nepomůže.
    * `plan_required`: obchod nemá na externí přístup nárok. Nemá tedy ani aktivní placený tarif,
      ani licenci, ani zkušební období s uloženou kartou (je třeba na tarifu Free, nebo ve
      zkušebním období bez karty). Po zaplacení tarifu nebo po doplnění karty se přístup otevře
      sám, bez nového klíče.
  * `key_surface_mismatch`: klíč je omezený na druhou plochu (jen REST API, nebo jen server MCP).
    Pro tuhle plochu je potřeba jiný klíč.
  * `shop_unavailable` znamená pozastavený nebo neaktivní obchod. Obvykle ho ale nedostanete:
    takový obchod odmítne dřív ochranná vrstva před API (viz oddíl **Když odpověď nemá tvar chyby
    API** níž).
* **402** `plan_required` vrací jediná cesta, `GET /api/v1/catalog-items`: obchod externí přístup
  má, ale jeho tarif nezahrnuje feedy dodavatelů (počet odběrů feedů je v tarifu nula), takže
  katalog dodavatelů nevidí. Je to jiná situace než `403 plan_required` výš, kdy obchod nemá nárok
  na celé API; rozlišujte je podle stavového kódu. Opakování nepomůže, katalog se otevře po
  přechodu na tarif s feedy dodavatelů. Ostatní cesty feedů dodavatelů tuhle chybu nevracejí,
  vydají to, co obchod má uložené (obchodu, který feedy nikdy neodebíral, prázdný výpis).
* **404** u `not_found` znamená, že záznam v obchodě není; u dokladu i to, že je stornovaný, protože
  stornovaný dobropis se přes API nevydává. U `shop_not_found` znamená, že na doméně, na kterou jste
  volali, žádný obchod neběží: nejčastěji je to shopzi.cz místo domény obchodu. Podívejte se na
  hostitele v adrese, ne na klíč.
* **422** vracejí cesty dokladu. `unsupported_document_type` znamená, že doklad existuje, ale daňový
  není (objednávka, cenová nabídka, účtenka), a nemá proto detail, PDF ani ISDOC. `isdoc_invalid`
  znamená, že by ISDOC neprošel pravidly formátu, typicky u dobropisu bez čísla opravovaného
  dokladu; zpráva říká proč. Ani jedno neopraví opakování.
* **429** `rate_limited` znamená, že klíč vyčerpal strop volání REST API za minutu nebo za den
  (oddíl **Objem požadavků** níž). Počkejte tolik sekund, kolik říká hlavička
  `Retry-After`, a pak pokračujte tam, kde jste skončili. Opakovat dřív nemá smysl: minutové okno
  je pevné a končí s celou minutou, takže každý další pokus před jejím koncem dostane znovu 429.
* **500** `server_error` je chyba na naší straně. Tady opakování smysl má, ale s odstupem, který se
  prodlužuje.
* **502** `pdf_unavailable` znamená, že se PDF dokladu teď nepodařilo vyrobit. Nic se nezměnilo;
  opakujte s odstupem.
* **503** `pdf_busy` přijde, když se PDF dokladu ještě nevyrábělo a server už vyrábí nejvíc souběžných
  PDF, kolik smí. Počkejte tolik sekund, kolik říká hlavička `Retry-After`, a zkuste to znovu.
  Stahujte PDF po jednom nebo po dvou, ne celou dávku naráz; doklad, jehož PDF už jednou vzniklo,
  přijde hned a tuhle odpověď nedostane.

> **Neplatný klíč, odvolaný klíč a vypršelý klíč vracejí totéž** Všechny tři jsou 401 `invalid_api_key` se stejným textem. Rozlišovat je by znamenalo prozradit tomu, kdo klíče zkouší, že jeden z nich existoval. Který stav klíč má, je vidět v administraci v seznamu klíčů. Chybějící hlavička a hodnota bez prefixu mají jiný kód, `missing_api_key`: o těch se rozhoduje bez databáze.

## Chyby přes MCP [#chyby-pres-mcp]

MCP server má vlastní tvar, protože mluví JSON-RPC, a je užitečné rozlišit tři vrstvy.

**Nepustila brána.** Všechno, co se rozhodne na vstupu, přijde jako obyčejná HTTP odpověď v tvaru
chyby REST API výš, ne jako odmítnutí nástroje: `403 origin_not_allowed` u požadavku s hlavičkou
`Origin` (prohlížečového klienta nepodporujeme, takže se odmítne každý původ), `401` u chybějícího
nebo neplatného klíče, `403` u zavřené plochy, chybějícího tarifu, pozastavení a klíče pro druhou
plochu, `404 shop_not_found` na doméně bez obchodu a `405 method_not_allowed` s hlavičkou
`Allow: POST` na `GET` a `DELETE`.

**Nástroj odmítl.** Přijde běžný úspěšný JSON-RPC výsledek s `isError: true`, českou větou
v `content` a strojovým kódem v `_meta` pod klíčem `cz.shopzi/errorCode`. `structuredContent`
v takové odpovědi **nikdy není**: výstupní schéma nástroje popisuje úspěch, takže chybový objekt
by na něm spadl a důvod odmítnutí by se k modelu vůbec nedostal. Kódy jsou `forbidden`,
`not_found`, `conflict`, `invalid_input`, `suspended`, `plan_required`, `confirmation_required`,
`confirmation_declined`, `approval_required`, `approval_pending`, `credit_cap_exceeded`,
`plan_change_limited`, `quote_changed`, `payment_declined` a `internal_error`.

* `confirmation_required` a `confirmation_declined` znamenají „zeptej se člověka a zavolej znovu“,
  ne „na tohle nemáš právo“, a hostitel, který by je nerozlišil, by uživateli tvrdil, že mu chybí
  oprávnění, které mu nechybí. `confirmation_required` přijde i tehdy, když schválení mezitím
  vypršelo nebo už bylo jednou použité: zavolejte nástroj znovu a vznikne nové.
* `approval_required` znamená, že akci musí schválit člověk v administraci obchodu. Odkaz na stránku
  schválení je v `_meta["cz.shopzi/approvalUrl"]`. Předejte ho uživateli a po schválení zavolejte
  nástroj znovu se stejnými argumenty.
* `approval_pending` znamená, že se na schválení pořád čeká. Odkaz je tentýž, nové schválení
  nevzniklo; neopakujte volání ve smyčce, počkejte na člověka. Jeden klíč má na schválení nejvýš
  20 čekajících žádostí a za hodinu jich založí nejvýš 30, ať skončily jakkoli; obojí je odmítnutí
  `conflict` s větou, na co počkat. Zamítnutá žádost vrátí `confirmation_declined`.
* `credit_cap_exceeded` znamená, že by akce překročila měsíční strop kreditů přístupového klíče
  (nový klíč má 2 000 kreditů měsíčně, pokud mu vlastník nenastaví jiný), nebo strop teď nejde
  ověřit. Nestrhlo se nic. Opakování pomůže až prvního dne dalšího měsíce, kdy se strop obnoví,
  nebo když vlastník obchodu strop klíče upraví v **Nastavení → API a MCP**
  ikonou **Upravit klíč**. Když je strop klíče nastavený na nulu, je utrácení pro ten klíč vypnuté
  a nový měsíc nepomůže. Když strop jen nešel ověřit, zkuste volání za chvíli znovu.
* `plan_required` znamená, že akci neumožňuje tarif obchodu. Vyčerpaný počet vlastních domén to
  není: ten mají všechny tarify stejný, takže přijde `conflict` s větou, že je potřeba nejdřív
  odebrat stávající doménu.
* `plan_change_limited` znamená, že se tarif obchodu přes MCP za posledních 24 hodin už měnil
  (nebo se právě mění). Nestrhlo se nic. Od kdy půjde další změna, je v
  `_meta["cz.shopzi/retryAt"]`; v administraci jde tarif změnit hned.
* `quote_changed` znamená, že potvrzená částka navýšení tarifu už neplatí: Stripe teď spočítal
  jinou, nabídka je starší než 90 minut, nebo patří k jiné periodě platby. Nestrhlo se nic.
  Zavolejte `upgrade` znovu bez `quote` a nechte potvrdit novou částku, starou nabídku neopakujte.
* `payment_declined` znamená, že karta uložená u SHOPZI platbu za navýšení tarifu odmítla. Tarif
  zůstal a nic se nestrhlo. Se stejnou kartou opakování nepomůže; kartu obchodník vymění
  v administraci v Nastavení → Předplatné tlačítkem **Spravovat platby**, a tahle věta je i ve
  zprávě odmítnutí.
* `suspended` je pojistka pro obchod, který se pozastavil během volání. Pozastavený obchod jinak
  neodpovídá už na vstupu (viz oddíl níž).
* `internal_error` je nečekaná chyba na naší straně. Věta je obecná schválně: podrobnosti zůstávají
  v našem protokolu, aby se ven nedostaly názvy tabulek ani cizí data. Zkuste to znovu s odstupem.

**Selhal protokol.** Pak je to opravdová JSON-RPC chyba s číselným kódem: `-32601` na neznámou
metodu (a na `subscriptions/listen`, které nevedeme, s HTTP 404), `-32602` na neplatné parametry
(sem spadá i podvržený nebo pozměněný `requestState` z potvrzovacího kola), `-32600` s HTTP 400 na
dávku, ve které je `completion/complete` nebo `subscriptions/listen`, `-32021` na chybějící
schopnost klienta, `-32022` na nepodporovanou verzi protokolu a `-32020`, když si hlavičky a tělo
požadavku odporují.

Chyba z databáze se ven nedostane nikdy v surové podobě: přeloží se na českou větu bez jmen
tabulek a omezení, a surový text zůstane v našem protokolu.

## Když odpověď nemá tvar chyby API [#kdyz-odpoved-nema-tvar-chyby-api]

Dva případy, kdy odpověď nevypadá jako chyby výš:

* **503 s tělem `{ "error": "Tento obchod je momentálně nedostupný." }`** posílá ochranná vrstva
  ještě před API. Znamená pozastavený nebo neaktivní obchod, nebo subdoménu shopzi.cz, na které
  žádný obchod neběží. Nemá pole `code` a nemá smysl ji parsovat jako chybu API.
* **Odpověď, která není JSON**, přišla od něčeho před aplikací: od ochranné vrstvy sítě, od proxy,
  nebo je to stránka obchodu, protože požadavek netrefil cestu `/api/v1`.
  Zkontrolujte adresu a hlavičky.

## Kde chybu dohledat [#kde-chybu-dohledat]

Volání, které dojde až k obchodu, si obchod zapíše do protokolu bez obsahu: čas, klíč, plochu,
operaci, výsledek s kódem chyby, stavový kód HTTP, dobu zpracování, kredity, IP adresu a klienta.
Vlastník nebo manažer obchodu ho najde v **Nastavení → API a MCP** v části
**Protokol volání**:

1. Nahoře zvolte období a filtry **Plocha**, **Výsledek** a **Klíč**. Volání, u kterých se klíč
   ověřit nepodařilo, mají v řádku klíče „Bez platného klíče“.
2. Ve sloupci **Výsledek** je, jak volání dopadlo (**Úspěch**, **Chyba**, **Odmítnuto**,
   **Neověřený klíč**, **Bez oprávnění**, **Strop kreditů**, **Nad limit**, **Čeká na potvrzení**,
   **Čeká na schválení**), a pod ním strojový kód chyby, tentýž, jaký dostal klient.

Protokol ukazuje nejvýš posledních 30 dní. Neúspěšných volání zapíše jen omezený počet (bez platného
klíče 60 za hodinu na obchod, s platným klíčem 1 000 za den na klíč a plochu); kolik jich za období
opravdu přišlo, ukazuje část **Využití**, kde se denní součty drží 90 dní. Odmítnutí hlavičkou
`Origin` a požadavky na doménu bez obchodu se nezapisují nikam, protože k nim žádný obchod nepatří.

## Objem požadavků [#objem-pozadavku]

REST API `/api/v1` počítá volání **na každý klíč zvlášť**, v minutovém a v denním okně. Strop se
řídí tarifem obchodu:

| Tarif                            | Volání za minutu | Volání za den |
| -------------------------------- | ---------------- | ------------- |
| Basic                            | 60               | 20 000        |
| Standard                         | 120              | 50 000        |
| Business                         | 240              | 100 000       |
| Enterprise a jednorázová licence | 300              | 200 000       |

Obě okna jsou pevná, ne klouzavá. Minutové začíná a končí s celou minutou, denní je kalendářní den
v českém čase a obnoví se o půlnoci v Praze. Dva klíče téhož obchodu se počítají odděleně, takže
rozběhnutá integrace nevyčerpá strop té druhé.

Počítají se jen volání, která prošla ověřením klíče, oprávnění, plochy a tarifu: tato odmítnutí
401 a 403 ze stropu neubírají (`inventory_disabled` ano, tu vrací až cesta sama). Každé
započítané volání jde do minutového okna, i to, které skončí 429; do denního jen ta, která
minutový strop pustil, takže klient, který chvíli nečekal, si tím nespálí zbytek dne.

Odpověď na volání, které prošlo všemi branami, tedy klíčem, plochou, tarifem i oprávněním, nese
hlavičky, podle kterých si klient sám určí tempo; nese je i odmítnutí 429:

* `X-RateLimit-Limit`: strop okna, které je právě těsnější (minutové, nebo denní);
* `X-RateLimit-Remaining`: kolik volání v tom okně ještě zbývá;
* `X-RateLimit-Reset`: kdy se to okno obnoví, jako Unix čas v sekundách.

Chybí jen ve výjimečné chvíli, kdy počítadlo nejde zapsat: volání pak projde bez omezení a hlavičky
by neměly z čeho vzniknout.

Nad strop přijde `429` s kódem `rate_limited` a navíc hlavičkou `Retry-After` v sekundách. Když je
vyčerpaný celý den, `Retry-After` míří až na půlnoc, ne na konec minuty.

Příliš hustý provoz z jedné IP adresy může navíc odmítnout ochranná vrstva sítě ještě před
aplikací; takové odmítnutí nemá tvar chyby API (viz oddíl výš). Držte se proto rozumné kadence
a synchronizujte přírůstkově podle článku [Stránkování a filtry](/dokumentace/vyvojari/strankovani-a-filtry),
místo abyste stahovali celý katalog v každém běhu.

**Volání nástrojů přes MCP mají strop také, ale jiný**: každé volání nástroje se počítá klíči,
kterým přišlo, v okně celé hodiny a kalendářního dne v českém čase, a strop se řídí tarifem obchodu:

| Tarif                            | Volání nástrojů za hodinu | Volání nástrojů za den |
| -------------------------------- | ------------------------- | ---------------------- |
| Basic                            | 600                       | 5 000                  |
| Standard                         | 1 200                     | 10 000                 |
| Business                         | 2 000                     | 20 000                 |
| Enterprise a jednorázová licence | 3 000                     | 30 000                 |

Do téhož stropu se počítá každé kolo, i to, které se jen ptá na potvrzení, a také našeptávání hodnot
(`completion/complete`); našeptávání nad strop neodmítne, jen vrátí prázdnou nabídku. Vedle toho má
obchod vlastní strop AI generování, do kterého se počítá i práce Agenta v administraci, a když je
vyčerpaný, odmítne i volání přes MCP. Odmítnutí je v obou případech odmítnutí nástroje s kódem
`conflict` a věta v něm říká, který ze dvou stropů narazil, aby šlo poznat rozběhnutou integraci od
vytíženého obchodu. Samo to pomine, takže tady opakování s odstupem smysl má.
