Przejdź do treści
Zapolu

17 czerwca 2026, Luboš Zápotočný

Webhooki się powtarzają. Załóż to w integracji.

Webhooki płatności i platform przychodzą w oknie ponowień co najmniej raz, a nie dokładnie raz. Co powoduje podwójne dostarczenie i jak neutralizują je idempotentne handlery.

Przeczytaj dokumentację webhooków dowolnego poważnego dostawcy (Stripe, Shopify, Adyen, GoPay), a znajdziesz w tej czy innej postaci to samo ostrzeżenie: dostarczenia mogą nastąpić więcej niż raz. W oknie ponowień kontrakt brzmi at-least-once, nie exactly-once; jeśli Twój endpoint pozostanie niedostępny przez całe okno, zdarzenie nie dotrze ani razu, dlatego pod wszystkim innym zabezpiecza je zadanie uzgadniające, które dociąga stan z API dostawcy. Dostarczanie dokładnie raz przez zawodną sieć jest niemożliwe, więc dostawcy stosują jedyne rozsądne rozwiązanie: kiedy nie mają pewności, że zdarzenie do Ciebie dotarło, wysyłają je jeszcze raz.

Większość integracji jest napisana tak, jakby tej klauzuli nie było. Działają na demo, działają w pierwszym miesiącu, a potem gdzieś między dostawcą a odpowiedzią 200 zdarza się timeout i to samo zdarzenie order.paid przychodzi dwa razy.

Jakie problemy powoduje podwójne dostarczenie

  • Klient dostaje dwa potwierdzenia zamówienia.
  • Stan magazynowy zostaje zdjęty dwa razy; tak zaczynają się widmowe „niedostępne”.
  • Faktura zostaje wystawiona dwukrotnie, co prędzej czy później wykryje księgowość.
  • Saldo lojalnościowe zostaje naliczone dwa razy.

Dostawca zrobił to, co udokumentował; handler założył obietnicę, której nikt nie złożył.

Idempotencja to całe rozwiązanie

Idempotentny handler daje ten sam stan końcowy niezależnie od tego, czy zdarzenie przetworzy raz, czy pięć razy. Ta jedna właściwość neutralizuje ponowienia, ręczne replaye i całą klasę wyścigów. Osiągnięcie jej to głównie dyscyplina:

  1. Nadaj każdemu zdarzeniu klucz. Dostawcy wysyłają ID zdarzenia; jeśli Twój nie wysyła, wyprowadź je ze stabilnych pól. Zapisuj przetworzone ID i pomijaj duplikaty. Wymuszaj to unikalnym constraintem w bazie, a nie w kodzie aplikacji: sprawdzanie „czy już to widziałem?” w kodzie aplikacji zostawia okno na wyścig, constraint nie zostawia.
  2. Wybieraj stan absolutny zamiast zmian względnych. „Ustaw status na opłacone” jest idempotentne z natury. „Zdejmij 2 sztuki ze stanu” nie jest. Kiedy dostawca wysyła delty, tłumacz je na upserty względem własnego zapisu zdarzenia zamiast bezwarunkowej arytmetyki.
  3. Zakładaj też brak kolejności. Ponowienia przychodzą również nie po kolei. order.updated potrafi wylądować przed order.created. Numery wersji albo znaczniki czasu na rekordzie docelowym („zastosuj tylko, jeśli nowsze”) rozwiązują to, czego założenia o kolejności nie zapewnią.

Reszta checklisty

Idempotencja to rdzeń; trzy nawyki wokół niej oddzielają integracje, które przeżywają produkcję, od tych, które produkują tickety:

  • Potwierdzaj szybko, przetwarzaj asynchronicznie. Zweryfikuj podpis, zapisz zdarzenie, zwróć 200, a właściwą pracę wykonuj z kolejki. Wolne synchroniczne handlery powodują timeouty, a timeouty kolejne ponowienia, których się obawiasz; jedno napędza drugie.
  • Uwierzytelniaj każdy webhook. Endpoint webhooka to nieuwierzytelniony URL w publicznym internecie, który zmienia stan Twojego biznesu. Większość dostawców podpisuje payload HMAC-iem, który przeliczasz i porównujesz; niektórzy (GoPay) dają tylko ID i oczekują, że sam pobierzesz miarodajny stan z ich API. Tak czy inaczej, nigdy nie działaj na surowym żądaniu w postaci, w jakiej przyszło.
  • Utrzymuj dead-letter queue z narzędziem do replayu. Część zdarzeń będzie zawodzić z powodów, których retry nie naprawi: usunięte SKU, luka w mapowaniu. Odstawiaj je na bok, alarmuj o nich i spraw, żeby ponowne odtworzenie jednego zdarzenia było przyciskiem, a nie sesją SSH. Kiedy zewnętrzne API jest niedostępne, replay pozwala nadrobić zaległości bez ogłaszania incydentu.

Test na wszystko powyższe jest zaskakująco prosty: weź wczorajsze zdarzenia ze stagingu i dostarcz każde z nich dwa razy, w przetasowanej kolejności. Jeśli stan końcowy zgadza się z produkcją, Twoja integracja rzeczywiście jest odporna na duplikaty.

To standard, według którego budujemy integracje. W dużej mierze właśnie to znaczy „zbudowane tak, żeby działać dalej, gdy API po drugiej stronie ma gorszy dzień” na naszej stronie usług, i to samo myślenie widać w naszym backendzie na zamówienie. A jeśli Twoje stany magazynowe już dryfują, warstwa webhooków to jedno z pierwszych miejsc, które warto sprawdzić.