# Dokumentace pro vývojáře

> Jak k obchodu na SHOPZI připojit vlastní program nebo asistenta: REST API jen ke čtení, MCP server, webhooky a založení obchodu z terminálu.

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

---

Tahle dokumentace je pro vás, pokud k obchodu na SHOPZI připojujete vlastní program nebo asistenta.
Popisuje rozhraní: adresy, hlavičky, oprávnění, tvary odpovědí a chyby. Jak vlastník obchodu přístup
zapne a vydá klíč, popisuje uživatelská dokumentace v článku [API a MCP](/dokumentace/nastaveni/api-a-mcp).

## Co obchod nabízí [#co-obchod-nabizi]

* **REST API** jen ke čtení, na doméně obchodu pod cestou `/api/v1`. Hodí se pro synchronizaci
  s účetnictvím, skladem nebo ERP. Cesty popisuje článek
  [Endpointy REST API](/dokumentace/vyvojari/rest-api-endpointy).
* **MCP server** na doméně obchodu pod cestou `/api/mcp`, přes který s obchodem pracuje asistent.
  Nastavení klienta popisuje článek
  [Připojení asistenta k obchodu](/dokumentace/vyvojari/pripojeni-asistenta).
* **Webhooky**, kterými obchod sám ohlásí změnu vašemu koncovému bodu. Co má umět příjemce, popisuje
  článek [Webhooky: příjem událostí](/dokumentace/vyvojari/webhooky).
* **Založení nového obchodu z terminálu** přes server MCP bez klíče na `https://shopzi.cz/api/mcp`,
  viz článek [Založení obchodu přes MCP](/dokumentace/vyvojari/zalozeni-obchodu-pres-mcp).

## Kde začít [#kde-zacit]

1. V článku [Rozhraní pro vývojáře](/dokumentace/vyvojari/rozhrani-prehled) zjistíte, čím se REST
   API a MCP server liší a co si připravit.
2. Vlastník obchodu otevře **Nastavení → API a MCP**, v části **Přístup a klíče**
   zapne v kartě **Externí přístup** plochu, kterou napojení potřebuje (**REST API**, **Server MCP**,
   nebo obě), a v kartě **API klíče** vydá klíč tlačítkem **Vytvořit klíč**.
3. Podle článku [Autentizace a oprávnění](/dokumentace/vyvojari/autentizace-a-opravneni) se klíčem
   prokážete a pošlete první požadavek.
4. Když požadavek selže, kód z odpovědi vysvětluje článek
   [Chybové odpovědi](/dokumentace/vyvojari/chybove-odpovedi).

Čtyři rychlé starty níž vedou od prázdného terminálu k prvnímu funkčnímu napojení. Příklady počítají
s obchodem na doméně `muj-eshop.cz` se subdoménou `muj-eshop`; dosaďte svoje.

## Rychlý start: asistent přes MCP [#rychly-start-asistent-pres-mcp]

Předpoklad: vlastník obchodu zapnul **Server MCP** a vydal klíč, který platí pro server MCP (v poli
**Kde klíč platí** volba **Jen server MCP**, nebo **REST API i server MCP**). Hotové nastavení klienta
ukazuje administrace v **Nastavení → API a MCP** dvakrát: v dialogu **Klíč je
vytvořený** hned po vytvoření klíče, kde je klíč už doplněný, a trvale v kartě **Připojení klienta**,
kde místo klíče stojí `VÁŠ_API_KLÍČ`. V obou se přepíná mezi **Claude Code**, **Cursor**, **VS Code**
a **Jiný klient**. Adresa serveru je doména, na které máte administraci otevřenou, s cestou `/api/mcp`.
Jméno serveru v klientovi se skládá z `shopzi-` a subdomény obchodu, aby šlo mít připojených víc
obchodů naráz.

Claude Code: příkaz spusťte v terminálu, server se přidá pro aktuální projekt.

```bash
claude mcp add --transport http shopzi-muj-eshop https://muj-eshop.cz/api/mcp --header "Authorization: Bearer VÁŠ_API_KLÍČ"
```

Cursor a jiní klienti: Cursor čte `~/.cursor/mcp.json` (všechny projekty) nebo `.cursor/mcp.json`
v projektu a stejný tvar `mcpServers` čte většina ostatních klientů.

```json
{
  "mcpServers": {
    "shopzi-muj-eshop": {
      "url": "https://muj-eshop.cz/api/mcp",
      "headers": {
        "Authorization": "Bearer VÁŠ_API_KLÍČ"
      }
    }
  }
}
```

VS Code: nastavení patří do `.vscode/mcp.json` a klíč se do souboru nepíše, VS Code se na něj zeptá
při spuštění serveru. Soubor tak jde bez obav uložit do repozitáře.

```json
{
  "inputs": [
    {
      "type": "promptString",
      "id": "shopzi-api-key",
      "description": "API klíč obchodu SHOPZI",
      "password": true
    }
  ],
  "servers": {
    "shopzi-muj-eshop": {
      "type": "http",
      "url": "https://muj-eshop.cz/api/mcp",
      "headers": {
        "Authorization": "Bearer ${input:shopzi-api-key}"
      }
    }
  }
}
```

Než se začnete ptát v chatu, ověřte spojení voláním `tools/list`. Zahajovací výměna se nečeká
a odpověď obsahuje jen nástroje, na které má klíč oprávnění. Jejich parametry popisuje reference,
například [MCP: katalog produktů](/dokumentace/vyvojari/mcp-katalog) nebo
[MCP: objednávky, vratky a doklady](/dokumentace/vyvojari/mcp-objednavky).

```bash
curl -s https://muj-eshop.cz/api/mcp \
  -H "Authorization: Bearer $SHOPZI_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
```

Obchod, který ještě neexistuje, nemá doménu ani klíč. K jeho založení se připojte k serveru bez klíče
na shopzi.cz; hlavičku `Authorization` neposílejte.

```bash
claude mcp add --transport http shopzi-zalozeni https://shopzi.cz/api/mcp
```

Nástroje tohoto serveru popisuje [MCP bez klíče: tarify a založení obchodu](/dokumentace/vyvojari/mcp-platforma)
a celý průběh až po předání klíče článek
[Založení obchodu přes MCP](/dokumentace/vyvojari/zalozeni-obchodu-pres-mcp). Transport, potvrzování
změn a stropy popisuje článek [MCP server](/dokumentace/vyvojari/mcp-server), časté potíže s klientem
[Připojení asistenta k obchodu](/dokumentace/vyvojari/pripojeni-asistenta).

## Rychlý start: první volání REST API [#rychly-start-prvni-volani-rest-api]

Předpoklad: vlastník obchodu zapnul v **Nastavení → API a MCP** **REST API**
a vydal klíč, který platí pro REST API (v poli **Kde klíč platí** volba **Jen REST API**, nebo
**REST API i server MCP**) a má oprávnění, které cesta chce. Výpis objednávek chce `orders:read`.
Adresa je doména obchodu s cestou `/api/v1` a všechny cesty odpovídají jen na `GET`.

Nejdřív klíč ověřte. Ping nevrací data obchodu, jen identitu klíče a jeho oprávnění, a stačí mu
libovolný platný klíč pro REST API. Totéž volání s vaším klíčem ukazuje i dialog **Klíč je
vytvořený** v části **Ověření přes REST API**:

```bash
export SHOPZI_API_KEY="shopzi_sk_..."

curl -s https://muj-eshop.cz/api/v1/ping \
  -H "Authorization: Bearer $SHOPZI_API_KEY"
```

Pak stáhněte první stránku objednávek:

```bash
curl -s "https://muj-eshop.cz/api/v1/orders?limit=50" \
  -H "Authorization: Bearer $SHOPZI_API_KEY"
```

Výpis vrací obálku `{ "data": [...], "next_cursor": "...", "next_updated_since": "..." }`. Dokud
`next_cursor` není `null`, pošlete jeho hodnotu beze změny v parametru `cursor` se stejnými filtry.
Kurzor je zapsaný v base64url, takže do adresy jde vložit bez kódování. Tady s pomocí `jq`:

```bash
CURSOR=$(curl -s "https://muj-eshop.cz/api/v1/orders?limit=50" \
  -H "Authorization: Bearer $SHOPZI_API_KEY" | jq -r '.next_cursor')

curl -s "https://muj-eshop.cz/api/v1/orders?limit=50&cursor=$CURSOR" \
  -H "Authorization: Bearer $SHOPZI_API_KEY"
```

Když odpověď místo dat nese `error`, rozhodujte se podle `error.code`, ne podle textu:
`insufficient_scope` znamená, že klíči chybí oprávnění, `external_access_not_enabled`, že REST API
není zapnuté. Všechny kódy vysvětluje článek [Chybové odpovědi](/dokumentace/vyvojari/chybove-odpovedi).
Pole, filtry a chyby každé cesty popisuje reference, například
[REST API: objednávky, vratky a zásilky](/dokumentace/vyvojari/rest-objednavky) a
[REST API: ověření klíče](/dokumentace/vyvojari/rest-klic).

## Rychlý start: průběžná synchronizace [#rychly-start-prubezna-synchronizace]

Synchronizace stojí na parametru `updated_since` REST API. Výpis s ním vrací jen záznamy změněné od
zadaného času včetně, od nejstarší změny, a průchod je stabilní: záznam, který se změní uprostřed
průchodu, se v něm neopakuje a přijde v příštím.

* **První běh** pošle `updated_since` hluboko v minulosti, třeba `2000-01-01T00:00:00Z`, ať přijde
  všechno.
* **Stránky** procházejte kurzorem. `next_cursor` posílejte zpátky se **stejným** `updated_since`
  i filtry; kurzor k jinému dotazu API odmítne kódem `400 invalid_cursor`.
* **Konec průchodu**: po poslední stránce si uložte `next_updated_since` jako `updated_since`
  příštího běhu. Je to začátek průchodu mínus 5 minut, takže příští běh
  vrátí poslední změny znovu. Záznamy proto ukládejte přepsáním podle `id`, nikdy přidáváním.
* **Navázání po pádu**: kurzor si ukládejte po každé zpracované stránce. Když běh spadne, pošlete
  uložený kurzor znovu se stejným `updated_since` a průchod doběhne k téže hranici. Když kurzor
  vrátí `400 invalid_cursor`, začněte průchod znovu od uloženého `updated_since`; přepsáním podle
  `id` se nic nezdvojí.
* **Odpověď 429** znamená, že klíč vyčerpal strop volání za minutu nebo za den. Počkejte tolik
  sekund, kolik říká hlavička `Retry-After`, a pošlete tentýž požadavek se stejným kurzorem. Po
  vyčerpání denního stropu míří `Retry-After` až na půlnoc českého času. Tempo si můžete řídit
  i dopředu podle hlaviček `X-RateLimit-Remaining` a `X-RateLimit-Reset`.
* **Odpověď 500** je chyba na naší straně: běh ukončete a zkuste ho znovu s prodlužujícím se
  odstupem. Uložený kurzor ho nechá navázat.

```javascript
// Stav mezi běhy: { since, cursor }. Ukládá se po každé stránce.
export async function syncOrders(state, { upsertById, saveState }) {
  for (;;) {
    const params = new URLSearchParams({ updated_since: state.since, limit: "100" });
    if (state.cursor) params.set("cursor", state.cursor);

    const res = await fetch(`https://muj-eshop.cz/api/v1/orders?${params}`, {
      headers: { Authorization: `Bearer ${process.env.SHOPZI_API_KEY}` },
    });

    if (res.status === 429) {
      const wait = Number(res.headers.get("retry-after") ?? "60");
      await new Promise((resolve) => setTimeout(resolve, wait * 1000));
      continue; // tentýž požadavek se stejným kurzorem
    }
    if (res.status === 400) {
      const { error } = await res.json();
      if (error.code !== "invalid_cursor" || !state.cursor) throw new Error(error.message);
      state.cursor = null; // průchod znovu od uloženého updated_since
      continue;
    }
    if (!res.ok) throw new Error(`SHOPZI ${res.status}`);

    const page = await res.json();
    await upsertById(page.data); // přepsat podle id, nikdy přidávat
    if (page.next_cursor) {
      state.cursor = page.next_cursor;
    } else {
      state.since = page.next_updated_since; // hranice příštího běhu
      state.cursor = null;
    }
    await saveState(state);
    if (!state.cursor) return state;
  }
}
```

Stejně se synchronizují produkty, kategorie, zákazníci, vratky, zásilky, zásoby, doklady, skladové
pohyby i data feedů dodavatelů (odběry, katalog dodavatelů, fronta ke schválení a dropship objednávky,
viz [REST API: feedy dodavatelů](/dokumentace/vyvojari/rest-feedy-dodavatelu)). Některé cesty stránkují podle času vzniku místo času změny a změnu ceny varianty čas
produktu neposouvá; obojí i s dalšími podrobnostmi popisuje článek
[Stránkování a filtry](/dokumentace/vyvojari/strankovani-a-filtry), stropy volání podle tarifu článek
[Chybové odpovědi](/dokumentace/vyvojari/chybove-odpovedi).

## Rychlý start: příjem webhooků [#rychly-start-prijem-webhooku]

Koncový bod zaregistruje vlastník nebo manažer obchodu v **Nastavení → API a MCP**
v části **Webhooky** tlačítkem **Přidat koncový bod**: vyplní **Adresu koncového bodu** s `https`
a zaškrtne události, které má dostávat. Hned po uložení se ukáže **Podpisové tajemství** s prefixem
`whsec_`, kterým si podpis ověříte; znovu ho zobrazí tlačítko
**Zobrazit tajemství**. Každá zpráva je `POST` s tělem `{ "id", "type", "created_at", "data" }`
a hlavičkami `webhook-id`, `webhook-timestamp` a `webhook-signature` podle specifikace Standard Webhooks.

Příjemce v Node.js bez závislostí: ověří razítko a podpis nad surovým tělem, odpoví hned a tutéž
zprávu zpracuje jen jednou.

```javascript
import { createHmac, timingSafeEqual } from "node:crypto";
import { createServer } from "node:http";

const SECRET = process.env.SHOPZI_WEBHOOK_SECRET; // whsec_...
const TOLERANCE_SEC = 300;
const processed = new Set(); // v provozu tabulka nebo fronta, ne paměť procesu

function verify(raw, headers) {
  const id = headers["webhook-id"];
  const timestamp = headers["webhook-timestamp"];
  const signature = headers["webhook-signature"];
  if (!id || !timestamp || !signature) return false;

  const age = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));
  if (!Number.isFinite(age) || age > TOLERANCE_SEC) return false;

  const key = Buffer.from(SECRET.replace(/^whsec_/, ""), "base64");
  const expected = Buffer.from(
    createHmac("sha256", key).update(`${id}.${timestamp}.${raw}`).digest("base64"),
  );
  return signature.split(" ").some((part) => {
    if (!part.startsWith("v1,")) return false;
    const presented = Buffer.from(part.slice(3));
    return presented.length === expected.length && timingSafeEqual(presented, expected);
  });
}

createServer((req, res) => {
  const chunks = [];
  req.on("data", (chunk) => chunks.push(chunk));
  req.on("end", () => {
    const raw = Buffer.concat(chunks).toString("utf8");
    if (req.method !== "POST" || !verify(raw, req.headers)) {
      res.writeHead(401).end();
      return;
    }
    res.writeHead(204).end(); // odpovědět hned, zpracovat potom

    const id = req.headers["webhook-id"];
    if (processed.has(id)) return; // táž zpráva podruhé
    processed.add(id);
    const event = JSON.parse(raw);
    console.log(event.type, event.data);
  });
}).listen(3000);
```

Co z toho plyne pro provoz:

* **Odpovězte do 10 sekund** kódem 2xx. Pomalejší odpověď, jiný kód i přesměrování se počítá jako
  nezdar; přesměrování nenásledujeme.
* **Po nezdaru posíláme znovu** po 1 minutě, 5 minutách, 30 minutách, 2 hodinách a 6 hodinách, celkem
  6 pokusů. Odpověď `410` říká, že koncový bod už neexistuje, a další
  pokus po ní nepřijde. Koncový bod, kterému za sebou 10
  doručení skončí nezdarem, se sám vypne.
* **Táž zpráva může dorazit vícekrát** se stejným `webhook-id`.
  Deduplikujte podle něj, ne podle obsahu těla.
* **Webhooky se řídí REST API.** Když je REST API obchodu zavřené (vypnutý přepínač, obchod bez
  nároku na externí přístup, tedy tarif Free nebo zkušební období bez uložené karty, pozastavení od
  SHOPZI, nebo REST API dočasně vypnuté pro všechny obchody), nové události se neposílají a po
  znovuotevření se nedoženou. Zprávy čekající na další pokus skončí v **Logu doručení** jako
  **Vzdáno** a po znovuotevření je jde poslat odkazem **Odeslat znovu**.

Seznam událostí, obsah pole `data`, zbytek smlouvy příjemce a postup pro Make.com a n8n popisuje
článek [Webhooky: příjem událostí](/dokumentace/vyvojari/webhooky).
