Przejdź do treści
Zapolu

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

Webhooki się powtarzają. Uwzględnij 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, a znajdziesz w tej czy innej postaci to samo ostrzeżenie. Stripe każe zapisywać ID przetworzonych zdarzeń, bo endpoint może otrzymać to samo zdarzenie więcej niż raz. Adyen każe spodziewać się tego samego zdarzenia dwa razy i obsłużyć duplikaty. Shopify udostępnia nagłówek X-Shopify-Webhook-Id do deduplikacji i pisze wprost, że dostarczenie nie jest w ogóle gwarantowane. Wszystkie trzy sprawdzone 13 lipca 2026: Stripe, Adyen, Shopify.

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 job uzgadniający, który pobiera stan z API dostawcy. Dostarczenie dokładnie raz przez zawodną sieć nie jest osiągalne, więc dostawcy wybierają wariant bezpieczny: 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ą w wersji 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 otrzymuje dwa potwierdzenia zamówienia.
  • Stan magazynowy zostaje zdjęty dwa razy i zaczynają się pojawiać nieprawidłowe oznaczenia „towar niedostępny”.
  • Faktura zostaje wystawiona dwa razy, a księgowość znajduje to miesiące później.
  • Punkty lojalnościowe naliczają się dwa razy; nikt tego nie zauważa, dopóki liczby nie przestaną się zgadzać.

Dostawca zrobił to, co udokumentował; handler liczył na gwarancję, której dostawca nigdy nie dał.

Idempotencja to 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ę race conditions. Osiągnięcie jej to głównie dyscyplina:

  1. Przypisz 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: sprawdzenie 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, a nie na bezwarunkową arytmetykę.
  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 rozwiązać nie mogą.

Reszta checklisty

Idempotencja to rdzeń. Trzy nawyki wokół niej obejmują resztę:

  • 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 powodują dokładnie te ponowienia, których się obawiasz.
  • 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 nie wysyłają payloadu w ogóle: powiadomienie GoPay to HTTP GET z ID płatności, a miarodajny stan trzeba odczytać z API GoPay, dopiero potem na nim działać (GoPay, sprawdzone 13 lipca 2026). 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 ponowienie nie naprawi: usunięte SKU, luka w mapowaniu. Odkładaj je tam, alarmuj o nich i umożliw odtworzenie pojedynczego zdarzenia bez logowania przez SSH na produkcję. Kiedy dostawca ma kilkugodzinną awarię, replay pozwala nadrobić zaległości bez ogłaszania incydentu.

Jeden test obejmuje wszystko powyższe: 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ą, Twoje handlery naprawdę są idempotentne.

To standard, według którego budujemy integracje. W dużej mierze to właśnie opisuje nasza strona o integracjach, 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ć.