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.
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.
Co obchod nabízí
- 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. - 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. - 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í.
- 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.
Kde začít
- V článku Rozhraní pro vývojáře zjistíte, čím se REST API a MCP server liší a co si připravit.
- 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íč.
- Podle článku Autentizace a oprávnění se klíčem prokážete a pošlete první požadavek.
- Když požadavek selže, kód z odpovědi vysvětluje článek Chybové odpovědi.
Č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
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.
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ů.
{
"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.
{
"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ů nebo
MCP: objednávky, vratky a doklady.
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.
claude mcp add --transport http shopzi-zalozeni https://shopzi.cz/api/mcpNástroje tohoto serveru popisuje MCP bez klíče: tarify a založení obchodu a celý průběh až po předání klíče článek Založení obchodu přes MCP. Transport, potvrzování změn a stropy popisuje článek MCP server, časté potíže s klientem Připojení asistenta k obchodu.
Rychlý start: první volání 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:
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:
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:
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.
Pole, filtry a chyby každé cesty popisuje reference, například
REST API: objednávky, vratky a zásilky a
REST API: ověření klíče.
Rychlý start: průběžná 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_sincehluboko v minulosti, třeba2000-01-01T00:00:00Z, ať přijde všechno. - Stránky procházejte kurzorem.
next_cursorposílejte zpátky se stejnýmupdated_sincei filtry; kurzor k jinému dotazu API odmítne kódem400 invalid_cursor. - Konec průchodu: po poslední stránce si uložte
next_updated_sincejakoupdated_sincepříš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 podleid, 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_sincea průchod doběhne k téže hranici. Když kurzor vrátí400 invalid_cursor, začněte průchod znovu od uloženéhoupdated_since; přepsáním podleidse 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-Afteraž na půlnoc českého času. Tempo si můžete řídit i dopředu podle hlavičekX-RateLimit-RemainingaX-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.
// 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ů). 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, stropy volání podle tarifu článek Chybové odpovědi.
Rychlý start: příjem webhooků
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.
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í.