Založte si e-shop zdarma a získejte 10 000 AI kreditů na start. Bez karty, bez závazku.E-shop zdarma + 10 000 AI kreditů na start

Začít zdarma
Přeskočit na obsah

Stránkování a filtry

Kurzorové stránkování, průběžná synchronizace přes updated_since, stropy na velikost stránky a filtry. Včetně toho, proč kurzor není pořadové číslo stránky.

Ověřeno 20. 10. 2026Markdown verze

Každý výpis vrací stejnou obálku:

{
  "data": [],
  "next_cursor": "eyJ2IjoxLCJtIjoicyIsImEiOiIyMDI2LTA5LTAxVDA4OjE0OjAyLjExODIwNFoiLCJp...",
  "next_updated_since": "2026-09-02T05:55:00.412000Z"
}

Když je next_cursor null, další stránka není. next_updated_since patří do příštího průchodu synchronizace, viz oddíl níž.

Kurzor, ne offset

Mezi dvěma vašimi požadavky mohou záznamy přibývat i se měnit. Offset by proto tiše přeskakoval: co bylo na pozici 25, je po jednom novém přírůstku na pozici 26 a druhá stránka ho nikdy neukáže.

Stránkuje se tedy kurzorem. Hodnotu next_cursor z odpovědi pošlete beze změny jako parametr cursor v dalším požadavku, se stejnými filtry a stejným updated_since, a nic nedopočítávejte.

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

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

Kurzor je neprůhledný řetězec. Nese pozici posledního řádku, hranici průchodu a otisk dotazu, ke kterému patří, ale je to vnitřní tvar a smí se změnit. Kurzor z jiné cesty, s jiným filtrem nebo s jiným updated_since API odmítne kódem 400 invalid_cursor, stejně jako kurzor, který se cestou poškodil. Takovou chybu neopakujte se stejným kurzorem: začněte průchod znovu bez něj.

Změna od 15. září 2026Do té doby byl kurzor dvojice času a identifikátoru oddělená svislítkem a čas v něm se zaokrouhloval na milisekundy, takže záznam vzniklý ve stejné milisekundě jako poslední řádek stránky mohl na hranici stránek vypadnout. Kurzor ve starém tvaru dnes vrátí 400 invalid_cursor a stačí průchod začít znovu. Nový kurzor je zapsaný v base64url, takže se v adrese nemusí nijak kódovat.

Velikost stránky

Parametr limit má výchozí hodnotu 25 a strop 100. Vyšší hodnota se ořízne, ne odmítne: požadavek projde a vrátí strop, takže vás špatně nastavený limit nezastaví, jen vám nedá víc.

Nesmyslná hodnota (0, záporné číslo, text) spadne na výchozí hodnotu. Nepočítejte tedy s tím, že vám API řekne, že jste limit napsali špatně.

limit do otisku dotazu v kurzoru nepatří, takže velikost stránky smíte mezi stránkami jednoho průchodu měnit. Filtry a updated_since ne.

Dvě pořadí: od nejnovějšího, nebo od poslední synchronizace

Bez updated_since vrací výpis záznamy od nejnovějšího podle času vzniku. To je pořadí pro jednorázový pohled na to, co přišlo naposledy.

S updated_since vrací jen záznamy změněné od té chvíle včetně, a to od nejstarší změny vzestupně. To je pořadí pro synchronizaci a dá se na něm stavět ze dvou důvodů:

  • Průchod je stabilní. První stránka si zapamatuje čas, kdy průchod začal, a všechny další stránky vracejí jen změny do té hranice. Záznam se v jednom průchodu nikdy nezopakuje. Když se změní poté, co ho průchod vydal, přijde znovu v příštím průchodu; když se změní dřív, než na něj došla řada, v tomhle průchodu nepřijde vůbec a přijde až v příštím, rovnou v nové podobě.
  • Mezi průchody se nic neztratí. next_updated_since je začátek průchodu mínus 5 minut. Překryv pokryje zápis, který v okamžiku začátku průchodu ještě běžel a potvrdil se až po něm. Cena je, že příští průchod vrátí změny posledních minut znovu, a proto záznamy ukládejte podle id (přepsáním), nikdy přidáváním.

Většina cest stránkuje podle updated_at. Záznamy, které se po vzniku nemění, stránkují podle created_at: GET /api/v1/documents a GET /api/v1/stock/movements. U každé cesty to říká i OpenAPI v rozšíření x-shopzi-sync-field.

Filtry

Filtry jsou u každé cesty vypsané ve sloupci Filtry v článku Endpointy REST API. Dají se kombinovat a skládají se konjunkcí, tedy platí všechny naráz.

  • updated_since bere ISO 8601 se zónou Z nebo s číselným posunem, například 2026-09-01T00:00:00Z nebo 2026-09-01T02:00:00+02:00, a přepne výpis do synchronizačního pořadí. Hodnotu next_updated_since z odpovědi vložte do adresy tak, jak přišla: je v UTC se Z, takže kódování nepotřebuje. Když posíláte vlastní čas s + a adresu nekódujete, + dorazí jako mezera; API ho přesto přečte jako +. Čas bez posunu vrátí 400 invalid_query.
  • status zužuje objednávky na stav objednávky, produkty na stav publikace, vratky na stav vratky, zásilky na stav zásilky a frontu ke schválení feedů dodavatelů na stav položky.
  • payment_status zužuje objednávky podle stavu platby.
  • doc_type zužuje doklady na druh (faktura, dobropis a další).
  • type zužuje skladové pohyby na druh pohybu.
  • kind zužuje frontu ke schválení feedů dodavatelů (/api/v1/supplier-approvals) na druh návrhu.
  • feed_id zužuje katalog dodavatelů (/api/v1/catalog-items) na jeden feed a include_gone=true do něj vrátí i položky, které z feedu zmizely (výchozí je false).
  • subscription_id zužuje běhy synchronizace (/api/v1/supplier-sync-runs) na jeden odběr.

Neznámá hodnota filtru je chyba, neznámý parametr se ignorujeHodnota, kterou výčet nezná (třeba status=zaplaceno), vrátí 400 invalid_query a zpráva vypíše povolené hodnoty. Do 15. září 2026 taková hodnota vracela prázdný výpis, který se od pravdy nedal odlišit. Překlep v názvu parametru ale chybu nevrátí, vrátí nezúžený výpis: když vám odpověď přijde větší, než jste čekali, je to první místo, kam se podívat.

Kdy stránkovat po velkých kusech

Strop 100 je maximum na požadavek, ne doporučení. Průběžná synchronizace si vystačí s malou stránkou, protože přírůstek je malý. Plný sesyp katalogu má naopak brát maximum: každá stránka je jeden dotaz do databáze obchodu, který v tu chvíli obsluhuje i zákazníky, a jedno volání proti stropu klíče za minutu a za den (čísla podle tarifu jsou v článku Chybové odpovědi).

Kde spotřebu volání uvidíte

Kolik volání synchronizace opravdu spotřebuje, ukazuje administrace obchodu v Nastavení → API a MCP:

  • v kartě API klíče ukazuje u každého klíče sloupec Naposledy i počet dnešních volání;
  • část Využití sečte volání, chyby, odmítnutí a volání nad limit za zvolené období, i po jednotlivých klíčích;
  • část Protokol volání ukáže jednotlivá volání i s cestou, výsledkem a dobou zpracování, takže poznáte například průchod, který se zacyklil na první stránce.

Za běhu si tempo řiďte podle hlaviček X-RateLimit-Remaining a X-RateLimit-Reset, které nese každá odpověď na volání, jež prošlo všemi branami.

Doporučený tvar synchronizace

  1. Při prvním běhu nastavte updated_since hluboko do minulosti, třeba 2000-01-01T00:00:00Z, ať přijde všechno.
  2. Dokud odpověď nese next_cursor, posílejte ho zpátky v cursor se stejným updated_since.
  3. Každý řádek uložte podle id: existující přepište, nový přidejte.
  4. Po poslední stránce si uložte next_updated_since z odpovědi jako hranici příštího běhu.
  5. Další běh začněte s updated_since rovným uložené hranici.
// SHOPZI_API_URL je adresa REST API obchodu i s cestou, třeba https://muj-eshop.cz/api/v1
async function syncOrders(since) {
  let cursor = null;
  for (;;) {
    const params = new URLSearchParams({ updated_since: since, limit: "100" });
    if (cursor) params.set("cursor", cursor);
    const res = await fetch(`${process.env.SHOPZI_API_URL}/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;
    }
    if (!res.ok) throw new Error(`Shopzi ${res.status}`);
    const page = await res.json();
    await upsertById(page.data);
    if (!page.next_cursor) return page.next_updated_since; // hranice příštího běhu
    cursor = page.next_cursor;
  }
}

Hranici berte z odpovědi, ne z hodin vlastního stroje: next_updated_since počítá databáze obchodu, takže nezávisí na tom, jak přesně jdou hodiny u vás. Jak navázat na průchod, který spadl uprostřed, ukazuje rychlý start na úvodní stránce Dokumentace pro vývojáře.

Smazání přes updated_since nepřijdeSmazaný záznam ve výpisu prostě chybí a změnou se neohlásí. Kdo potřebuje smazané záznamy odstranit i u sebe, projde jednou za čas celý výpis bez updated_since a porovná identifikátory. U produktů pomůže i událost product.deleted přes webhooky; u ostatních záznamů událost o smazání není. Položka katalogu dodavatelů se nemaže: když z feedu zmizí, dostane čas v gone_at, posune se jí updated_at a z výchozího výpisu /api/v1/catalog-items vypadne. Synchronizace, která ji má zachytit, proto posílá include_gone=true; totéž ohlásí webhook catalog_item.gone. Stejný celý průchod zachytí i změny, které čas updated_at neposouvají, třeba cenu varianty u produktu.

Časté potíže

  • Průchod se točí pořád na první stránce. Kurzor se do adresy nedostal: klient ho neposílá, nebo ho posílá pod jiným jménem než cursor, a neznámý parametr se ignoruje. Poškozený kurzor by API odmítlo kódem 400 invalid_cursor, takže smyčka bez chyby znamená chybějící kurzor. next_cursor je v base64url a do adresy patří beze změny.
  • 400 invalid_cursor uprostřed průchodu. Mezi stránkami se změnil filtr nebo updated_since, nebo kurzor patří jiné cestě. Začněte průchod znovu bez cursor; přepsáním podle id se nic nezdvojí.
  • 400 invalid_query u updated_since. Čas nemá časový posun (2026-09-01T00:00:00 bez Z), nebo je to jen datum. Pošlete 2026-09-01T00:00:00Z nebo hodnotu next_updated_since beze změny.
  • Výpis je větší, než jste čekali. Překlep v názvu parametru se ignoruje a výpis zůstane nezúžený. Zkontrolujte názvy filtrů podle článku Endpointy REST API.
  • Synchronizace nezachytila změnu ceny nebo zásoby. Cena varianty, obrázek ani zásoba čas updated_at produktu neposouvají. Zásoby synchronizujte přes /api/v1/stock a katalog jednou za čas projděte celý, bez updated_since.