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

Webhooky: příjem událostí

Smlouva pro příjemce: jaké události koncový bod odebírá, jak se ověří podpis, jak se opakuje doručení a proč musí být příjem idempotentní.

Ověřeno 20. 10. 2026Markdown verze

Webhook je opačný směr než API: ne váš program se ptá nás, ale obchod zavolá vám. Koncový bod se registruje v Nastavení → API a MCP v části Webhooky, přijímáme jen https a adresy do vnitřní sítě odmítáme.

Tenhle článek popisuje smlouvu příjemce, tedy co má vaše strana umět, aby zprávu přijala správně a bezpečně.

Hromadné změny událost neposílajíUdálosti vypouštějí jednotlivé změny: jedna objednávka, jeden produkt, jeden doklad. Hromadné úkony to schválně nedělají, a to import produktů, hromadná změna stavu i hromadné odstranění: jedna zpráva na řádek by u tisícové dávky znamenala tisíce webhooků během několika vteřin, váš příjemce by narazil na vlastní strop a koncový bod by se po deseti selháních sám vypnul. Po hromadné změně si katalog načtěte z REST API. Docházející zásoba je ze stejného důvodu jedna denní zpráva se seznamem, ne zpráva na položku, a synchronizace feedů dodavatelů posílá místo zpráv na produkt souhrny za běh (stock.changed, catalog_item.gone).

Registrace koncového bodu

Koncový bod zakládá vlastník nebo manažer obchodu v Nastavení → API a MCP v části Webhooky, v kartě Odchozí webhooky:

  1. Klikněte na Přidat koncový bod.
  2. Do pole Adresa koncového bodu vložte adresu, na kterou mají zprávy chodit, třeba URL z modulu Custom webhook v Make.com nebo Production URL uzlu Webhook v n8n. Adresa musí začínat https:// a nesmí vést do vnitřní sítě.
  3. Do pole Popis (nepovinné) můžete napsat, k čemu koncový bod slouží, ať ho v seznamu poznáte.
  4. Zaškrtněte události ve skupinách Objednávky, Katalog a sklad, Zákazníci a doklady a Feedy dodavatelů. Předvybraná je order.paid; aspoň jedna událost musí zůstat zaškrtnutá.
  5. Klikněte na Přidat koncový bod. Hned potom se ukáže Podpisové tajemství s prefixem whsec_ a tlačítkem Kopírovat. Vložte ho k příjemci; znovu ho zobrazí jen tlačítko Zobrazit tajemství v administraci.

U hotového koncového bodu jsou tlačítka Odeslat test, Zobrazit tajemství, Nové tajemství, Pozastavit (u pozastaveného Zapnout), Log doručení a Smazat. Smazání odstraní i log doručení a odesílání přestane okamžitě.

Webhooky běží pod REST API

Zpráva webhooku říká „stalo se tohle“ a podrobnosti si příjemce dočte klíčem z REST API. Webhooky se proto řídí tím, jestli je REST API obchodu otevřené, podle stejných pravidel jako volání klíčem: plocha REST API je obchodu zapnutá, obchod má nárok na externí přístup (aktivní placený tarif, jednorázovou licenci, nebo zkušební období s uloženou kartou) a SHOPZI externí přístup obchodu nepozastavilo ani REST API dočasně nevypnulo všem obchodům. Pravidla podrobně popisuje článek Autentizace a oprávnění.

Když je REST API zavřené:

  • Nové události se neodesílají. Co se v obchodě mezitím stane, nepřijde ani po znovuotevření; od té chvíle chodí jen nové události. Stav, který jste během pauzy nedostali, si dočtěte z REST API.
  • Zprávy čekající ve frontě nebo na další pokus se neodešlou. V Logu doručení skončí ve stavu Vzdáno s důvodem „Webhooky jsou pozastavené, externí API je vypnuté nebo tarif API nezahrnuje“. Po znovuotevření je jde poslat odkazem Odeslat znovu.
  • Testovací zprávu, opakované odeslání ani nový koncový bod administrace nedovolí a karta webhooků ukáže, že jsou pozastavené a proč, i s odkazem Přejít na přepínače.

Události

Koncový bod odebírá jen ty typy, které mu zaškrtnete. Celkem jich je 23:

Typ událostiKdy se posílá
order.createdObjednávka vytvořena
order.paidObjednávka zaplacena
order.fulfilledObjednávka vyřízena
order.shippedObjednávka odeslána
order.deliveredObjednávka doručena
order.cancelledObjednávka zrušena
order.refundedObjednávka refundována
product.createdProdukt vytvořen
product.updatedProdukt upraven
product.deletedProdukt smazán
customer.createdNový zákazník
stock.lowDocházející zásoba
document.issuedVystaven doklad
return.requestedŽádost o vrácení / reklamace
supplier_order.sentObjednávka odeslána dodavateli (dropship)
supplier_order.confirmedDodavatel objednávku potvrdil
supplier_order.shippedDodavatel odeslal zákazníkovi
supplier_order.rejectedDodavatel nemůže dodat
supplier_sync.completedSynchronizace feedu něco změnila
supplier_sync.failedSynchronizace feedu selhala
supplier_approval.pendingNové položky ke schválení z feedu
catalog_item.goneDodavatel vyřadil zboží z feedu
stock.changedZměna skladu z feedu (souhrn za běh)

Tvar zprávy

Každá zpráva je POST s tělem v JSON (Content-Type: application/json; charset=utf-8) a obálka má vždy tytéž čtyři klíče:

{
  "id": "7b1e4c2a-3f5d-4e8b-9c61-2d0a5f7e9b13",
  "type": "order.paid",
  "created_at": "2026-09-01T08:14:02.118Z",
  "data": {}
}
  • id je identifikátor zprávy, tentýž jako v hlavičce webhook-id. Nemění se při opakování doručení ani při ručním odeslání znovu.
  • type je typ události z tabulky výš a podle něj se rozhodujte.
  • created_at je čas vzniku události, ISO 8601 v UTC s milisekundami.
  • data nese údaje události; podrobnosti si dočtěte z REST API.

Vedle podpisových hlaviček nese zpráva i hlavičku shopzi-event s typem události, podle které jde v Make nebo v n8n směrovat bez čtení těla. Podepsaná není, takže rozhoduje vždycky type v podepsaném těle. Hlavička User-Agent má hodnotu ShopziWebhooks/1.0 (+https://shopzi.cz).

Co nese pole data

Obsah data je schválně hubený: identifikátory, stav a peníze, ne celý záznam. Celý záznam má stabilní tvar v REST API, kam si pro něj sáhnete klíčem. Částka total_minor je celé číslo v haléřích, stejně jako v REST API.

UdálostKlíč v dataPole
order.* (všech sedm)orderid, order_number, status, payment_status, total_minor, currency_code, customer_id, is_quotation, placed_at, created_at
product.created, product.updatedproductid, title, slug, status, product_type, created_at, updated_at
product.deletedproductid, title, slug (záznam už neexistuje)
customer.createdcustomerid, email, first_name, last_name, is_business, company_name, newsletter_subscribed, created_at
document.issueddocumentid, doc_type, doc_number, order_id, customer_id, duzp, created_at
return.requestedreturnid, return_number, order_id, customer_id, regime, status, created_at
stock.low(bez obalu)count a items, v každé položce variant_id, sku, title, available, threshold
supplier_order.sent, supplier_order.confirmed, supplier_order.shipped, supplier_order.rejectedsupplier_orderid, order_id, order_number, catalog_supplier_id, status, tracking_number, carrier, updated_at
supplier_sync.completed, supplier_sync.failedsupplier_sync_runid, subscription_id, trigger, status, started_at, finished_at, items_seen, created, updated, hidden, unhidden, held, skipped, errors, error
supplier_approval.pendingsupplier_approvalsadded (kolik přibylo), pending_total (kolik čeká celkem), subscription_id, source (sync nebo supplier_order)
stock.changed(bez obalu)supplier_sync_run_id, subscription_id, source (supplier_feed), part, parts a variants, v každé položce variant_id, product_id, stock
catalog_item.gone(bez obalu)supplier_sync_run_id, subscription_id, source (supplier_feed), part, parts a items, v každé položce catalog_item_id, external_id, product_id, variant_id

supplier_sync.completed odchází jen z běhu, který něco změnil nebo odložil ke schválení: hodinový běh, který nic nového nenašel, událost nevypustí. supplier_sync.failed odchází z každého běhu, který selhal. supplier_approval.pending přichází jednou za dávku (běh synchronizace, objednávka čekající na odeslání dodavateli), ne za každou položku; samotnou frontu si přečtěte přes /api/v1/supplier-approvals?status=pending.

stock.changed a catalog_item.gone jsou souhrny za jeden běh synchronizace feedu: synchronizace cen a skladu schválně nevypouští product.updated, protože u stovek obchodů by šlo o miliony zpráv za hodinu. stock.changed vyjmenuje varianty, kterým se v běhu změnil sklad dodavatele, catalog_item.gone zboží, které dodavatel z feedu vyřadil (každé vyřazení přijde jednou). Jedna zpráva nese nejvýš 500 položek; větší běh pošle několik zpráv, part a parts říkají kolikátá je.

Časy uvnitř data mají tvar jako v REST API: ISO 8601 v UTC se zónou Z a s mikrosekundami (2026-09-01T08:14:02.118204Z), takže čas z webhooku a čas téhož záznamu z REST API jsou tentýž řetězec. Obálka má u created_at jen milisekundy. Datum bez času (duzp u dokladu) má tvar RRRR-MM-DD.

Změna od 29. září 2026Do té doby chodily časy v data v textovém tvaru databáze, tedy 2026-09-01 08:14:02.118204+00 (mezera místo T a posun bez minut). Parser data a času si poradí s oběma tvary; kdo čas z webhooku porovnával nebo ukládal jako text, má si ho teď převést na skutečný čas.

Událost, která se jednomu záznamu stane jednou (objednávka v daném stavu, nový produkt, nový zákazník, vystavený doklad, žádost o vrácení), má identifikátor odvozený z toho záznamu a na jeden koncový bod odejde nejvýš jednou; kdyby ji obchod vypustil znovu, nese stejné id. Totéž platí pro dropship objednávku u dodavatele: každý její stav (odeslána dodavateli, potvrzena dodavatelem, odeslána zákazníkovi, dodavatel nemůže dodat) má jeden identifikátor. Adresa zákazníka ve zprávě o dropship objednávce není, ta je u objednávky. Běh synchronizace má jeden identifikátor pro supplier_sync.completed nebo supplier_sync.failed a jeden pro supplier_approval.pending, souhrny běhu (stock.changed, catalog_item.gone) mají identifikátor z běhu a pořadí dávky. product.updated a stock.low dostávají pokaždé nový identifikátor, protože každá úprava a každé denní hlášení je nová událost.

order.refunded má identifikátor odvozený z vrácení peněz, ne z objednávky. Každá úspěšná refundace je samostatná zpráva: dvě částečné refundace téže objednávky jsou dvě zprávy s různým id, a kdyby obchod tutéž refundaci vypustil znovu, nese stejné id. Zprávu pošle každé vrácení peněz, které je hotové:

  • refundace přes platební bránu (Comgate, Stripe), kterou brána potvrdí rovnou,
  • refundace, kterou brána zpracovává a dokončí až později: zpráva odejde ve chvíli, kdy brána potvrdí, že peníze odešly,
  • ruční refundace, kterou obchodník u vratky zapíše, když peníze pošle sám (dobírka, převod).

Refundace, kterou brána teprve zpracovává nebo kterou odmítla, zprávu nepošle. Zpráva nese stav objednávky po refundaci: payment_status je partially_refunded, nebo refunded, když se vrátila celá zaplacená částka. U objednávky, kterou obchod jako zaplacenou nevedl (třeba dobírka, kterou nespároval), zůstane payment_status beze změny. Vrácené částky podle vratek najdete v /api/v1/returns (refund.refunded_minor).

Hlavičky a podpis

Podepisujeme podle otevřené specifikace Standard Webhooks. Zpráva nese hlavičky webhook-id, webhook-timestamp a webhook-signature.

Podepisovaný řetězec je {id}.{timestamp}.{payload}, kde payload je surové tělo požadavku, bajt po bajtu. Klíčem je base64 dekódovaná část tajemství za prefixem whsec_, algoritmus je HMAC-SHA256 a posílá se jako v1,<base64>. Podle specifikace smí hlavička nést víc podpisů oddělených mezerou. SHOPZI dnes posílá jeden, ale ověření má projít všechny a zprávu přijmout, když sedí kterýkoli s verzí v1.

import { createHmac, timingSafeEqual } from "node:crypto";

export function overitPodpis(raw, headers, secret, toleranceSec) {
  const id = headers["webhook-id"];
  const timestamp = headers["webhook-timestamp"];
  const presented = headers["webhook-signature"];
  if (!id || !timestamp || !presented) return false;

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

  const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
  const expected = createHmac("sha256", key)
    .update(`${id}.${timestamp}.${raw}`, "utf8")
    .digest("base64");

  return presented.split(" ").some((part) => {
    if (!part.startsWith("v1,")) return false;
    const value = part.slice(3);
    if (value.length !== expected.length) return false;
    return timingSafeEqual(Buffer.from(value), Buffer.from(expected));
  });
}

Surové tělo, ne rozparsovaný objektPodepsané jsou bajty, které dorazily. Kdo tělo napřed rozparsuje na JSON a pak ho zpátky serializuje, podepisuje jiný řetězec a podpis mu nikdy nesedne. V n8n je to přepínač Raw Body, v Express express.raw(), ve frameworku, který parsuje sám, obvykle vypnutí parseru na téhle jedné cestě.

Razítko a přehrávání

Tolerance je 5 minut na obě strany. Zprávu s razítkem mimo toleranci zahoďte, i kdyby podpis seděl: bez téhle kontroly je jednou odchycený požadavek přehratelný navždy. Ověřujte v pořadí razítko, pak podpis, a porovnávejte v konstantním čase.

Opakování a idempotence

Doručujeme nejméně jednou. Za úspěch se počítá jen odpověď 2xx, a to do 10 sekund. Jiný stavový kód, vypršený čas i nedostupná adresa jsou nezdar, stejně jako přesměrování (3xx): přesměrování nenásledujeme, adresu koncového bodu zadejte přímo. Po nezdaru posíláme znovu, a to po 1 minutě, 5 minutách, 30 minutách, 2 hodinách a 6 hodinách. Pokusů je celkem 6, pak zprávu vzdáme.

Odpověď 410 Gone bereme jako vzkaz, že koncový bod už neexistuje, a další pokus po ní nepřijde. Koncový bod, kterému za sebou skončí nezdarem 10 doručení, se sám vypne: v administraci dostane označení Vypnuto pro opakované chyby a obchod dostane upozornění Odchozí webhook byl vypnut. Úspěšné doručení počítadlo nezdarů vynuluje a stejně tak ruční zapnutí tlačítkem Zapnout.

Z toho plyne jediné pravidlo, které nesmíte porušit: táž zpráva vám může dorazit vícekrát. Deduplikujte podle webhook-id, které je součástí podpisu, a ne podle obsahu těla.

Odpovězte hned a zpracujte potomZapište zprávu do své fronty a odpovězte 2xx. Na odpověď čekáme 10 sekund, takže kdo si v obsluze webhooku pustí půlminutovou synchronizaci, dostane místo úspěchu nezdar a opakování, a pak řeší dvě běžící naráz.

Testovací zpráva a log doručení

Testovací zprávu posílá tlačítko Odeslat test u koncového bodu v Nastavení → API a MCP, v části Webhooky. Použijte ho dřív, než napojení pustíte naostro: ověří, že vaše adresa odpovídá, že jde ověřit podpis a že fronta odpoví včas. Make si z ní navíc načte strukturu dat. Testovací zpráva je podepsaná jako každá jiná, má typ order.created a v data nese "test": true, větu note a smyšlenou objednávku TEST-1 na 1 299 Kč (total_minor: 129900). Odchází hned, i když koncový bod událost order.created neodebírá, a na skutečné objednávky nesahá.

Historii pokusů ukáže Log doručení u téhož koncového bodu: stav (Ve frontě, Doručeno, Neúspěch, zkusíme znovu, Vzdáno), stavový kód odpovědi a počet pokusů. Podrobnosti ukážou ID zprávy, odeslané tělo a odpověď protistrany, Odeslat znovu pošle tutéž zprávu znovu se stejným id.

Tajemství se dá kdykoli přegenerovat tlačítkem Nové tajemství (a potvrzením Vygenerovat nové). Staré tím okamžitě přestane platit, takže se nová hodnota nasazuje na vaší straně jako první.

Časté potíže

  • „Adresa musí začínat https://.“ Nešifrované http nepřijímáme, stejně jako adresy do vnitřní sítě nebo na localhost.
  • Nic nechodí a karta hlásí, že jsou webhooky pozastavené. REST API obchodu je zavřené (vypnutý přepínač, tarif bez externího přístupu, zásah SHOPZI). Po znovuotevření chodí jen nové události; vzdané zprávy pošlete znovu z Logu doručení.
  • Podpis nikdy nesedí. Ověřujete rozparsované a znovu serializované tělo místo surového, nebo jste po Novém tajemství nenasadili novou hodnotu. Zkontrolujte i to, že klíč HMAC je base64 dekódovaná část tajemství za whsec_, ne celý řetězec jako text.
  • Zprávy se odmítají jako staré. Hodiny příjemce jdou o víc než 5 minut jinak, nebo zprávu zpracováváte z fronty příliš pozdě. Razítko ověřujte hned při příjmu.
  • V logu je nezdar s přesměrováním. Adresa vrací 3xx, například na variantu s lomítkem na konci nebo na jinou doménu. Přesměrování nenásledujeme, zadejte cílovou adresu přímo.
  • Koncový bod je „Vypnuto pro opakované chyby“. Za sebou skončilo nezdarem 10 doručení. Opravte příjemce a klikněte na Zapnout; počítadlo nezdarů se tím vynuluje.

Obchodnickou verzi téhle stránky, tedy postup klikáním včetně napojení na Make.com a n8n, najdete v článku API klíče, webhooky a napojení. Hotový příjemce v Node.js je v rychlém startu na úvodní stránce Dokumentace pro vývojáře.