Formularz wysłał zdarzenie do integracji. Odbiornik zapisał leada, lecz odpowiedź nie dotarła do nadawcy przed upływem czasu. Nadawca widzi nieudaną próbę. Ponowienie tego samego żądania może teraz utworzyć drugiego leada, chociaż klient wypełnił formularz tylko raz.
Problem zaczyna się od kontraktu: trzeba rozróżnić zdarzenie biznesowe, próbę jego dostarczenia oraz wykonany skutek. Sam kod HTTP nie opisuje wszystkich trzech. Poniższy model jest propozycją projektową dla integracji; konkretne nagłówki, limity i zasady ponowień trzeba sprawdzić u danego dostawcy.
Najpierw poznaj zasady nadawcy
Nie każdy webhook jest ponawiany automatycznie. GitHub dokumentuje brak automatycznej ponownej dostawy po błędzie i udostępnia mechanizmy ręczne oraz API do zbudowania własnej obsługi. Dlatego plan odzyskiwania zdarzeń musi obejmować także sytuację, w której kolejne żądanie nigdy samo nie nadejdzie. GitHub Docs: Handling failed webhook deliveries.
Stripe opisuje z kolei możliwość otrzymania zdarzenia więcej niż raz oraz brak gwarancji kolejności dostaw. To dobry przykład, dlaczego strategii jednej integracji nie należy kopiować do każdej następnej. Stripe: Webhooks.
Przed implementacją zapisz, kto ponawia żądanie, jaki identyfikator pozostaje stały, jak długo można odtworzyć zdarzenie i co oznacza odpowiedź sukcesu. Ustal też, czy integracja musi obsłużyć ręczne wznowienie po kilku dniach. Okres przechowywania identyfikatorów powinien wynikać z tego kontraktu, a nie z przypadkowego czasu życia cache.
Nadaj zdarzeniu stabilną tożsamość
Własna integracja może przekazywać poniższą kopertę. To przykładowy format, a nie payload wymagany przez GitHub, Stripe ani istniejący formularz Corecorp.
{
"eventId": "lead-demo-0042",
"eventType": "lead.created",
"schemaVersion": 1,
"entityId": "lead-0042",
"occurredAt": "2026-09-09T10:00:00Z"
}eventId pozostaje ten sam przy ponowieniu tego zdarzenia. Numer próby jest inną informacją i może rosnąć. entityId identyfikuje obiekt: dwa różne zdarzenia dotyczące jednego leada nie powinny zostać automatycznie uznane za duplikaty.
Przy wielu integracjach klucz deduplikacji powinien uwzględniać również źródło i jego zakres, na przykład konkretną instalację. Identyfikator z obcego systemu nie musi być globalnie unikalny dla całej aplikacji. Dla GitHub warto znać zachowanie X-GitHub-Delivery: przy zamówionej ponownej dostawie nagłówek zachowuje wartość z pierwotnej próby. GitHub Docs: Best practices for using webhooks.
Zapisz przyjęcie atomowo
Sekwencja „sprawdź, czy identyfikator istnieje; jeśli nie, zapisz” ma lukę, gdy dwa żądania wykonają sprawdzenie równocześnie. Potrzebna jest gwarancja na poziomie wspólnego magazynu danych. W relacyjnej bazie jednym z narzędzi jest ograniczenie unikalności obejmujące kolumny klucza; PostgreSQL pozwala definiować takie ograniczenie dla grupy kolumn. PostgreSQL: Unique Constraints.
Kolumny tego klucza powinny być wymagane i pozbawione pustych identyfikatorów. Domyślne ograniczenie unikalności PostgreSQL dopuszcza więcej niż jeden wiersz z NULL w kolumnie klucza; same wartości nie mogą więc pozostać opcjonalne w tym modelu.
Dla odbiornika z kolejką proponujemy następującą granicę przyjęcia:
zweryfikuj autentyczność i poprawność zdarzenia
rozpocznij transakcję
zapisz zdarzenie ze wspólnym unikalnym kluczem
jeżeli istnieje: rozpoznaj wcześniejszy stan przyjęcia
jeżeli jest nowe: zapisz trwałe zadanie do wykonania
zatwierdź transakcję
potwierdź przyjęcie nadawcyTo opis kolejności, nie gotowy kod do wdrożenia. Rejestr zdarzenia i zadanie muszą znaleźć się w tej samej transakcji albo korzystać z równoważnego mechanizmu zapewniającego spójność. W przeciwnym razie awaria między dwoma zapisami może pozostawić oznaczenie „widziane” bez pracy, która miała zostać wykonana.
Duplikat już przyjętego zdarzenia nie powinien tworzyć nowego zadania. Nadal trzeba jednak odróżniać stan oczekujący od zakończonego i obsłużyć awarię wykonawcy. Zwrócenie sukcesu oznacza wtedy trwałe przyjęcie do przetwarzania, a nie obietnicę, że CRM został już zaktualizowany.
Oddziel zapis w bazie od skutku zewnętrznego
Transakcja we własnej bazie nie obejmuje automatycznie obcego API ani wysłania e-maila. Jeśli worker wykona operację zewnętrzną i zatrzyma się przed zapisaniem wyniku, następny worker może spróbować jeszcze raz.
Dla każdego skutku ustal własną ochronę: stabilny klucz idempotencji obsługiwany przez docelowe API, zapis pod znanym identyfikatorem albo procedurę uzgadniania stanu. Jeżeli system docelowy nie oferuje takiego mechanizmu, nazwij ograniczenie w projekcie integracji. Sam rejestr webhooków nie daje gwarancji jednokrotnego wykonania operacji w dowolnym zewnętrznym systemie.
Nie sprowadzaj też bezpieczeństwa do deduplikacji. Najpierw trzeba potwierdzić pochodzenie danych i zweryfikować podpis zgodnie z dokumentacją nadawcy. GitHub zaleca sekret webhooka i HTTPS; sekret nie powinien trafiać do adresu URL. GitHub Docs: Best practices.
Odbierz integrację przez scenariusze awarii
Przygotuj środowisko z kontrolowanym odbiornikiem, aby test nie utworzył prawdziwych leadów. Dla jednego zdarzenia wykonaj dostawę zwykłą, powtórzoną oraz dwie próby równoległe. Następnie przerwij worker przed operacją docelową i po niej. Porównuj liczbę zdarzeń, zadań oraz skutków, nie tylko odpowiedzi HTTP.
| Scenariusz | Oczekiwany dowód |
|---|---|
| Dwie równoległe dostawy | Jedno przyjęte zdarzenie i jedno nowe zadanie |
| Worker przerywa przed skutkiem | Zadanie może wrócić do wykonania |
| Timeout po skutku zewnętrznym | Uzgodnienie stanu lub skuteczna idempotencja celu |
| Nieznany typ zdarzenia | Jawna decyzja kontraktu, bez przypadkowej operacji |
W logach wystarczą identyfikatory korelacji, typ i stan. Pełna treść zapytania z formularza zwykle nie jest potrzebna do policzenia ponowień. Powiązanie tego modelu z jakością danych formularza pomaga oddzielić duplikat techniczny od dwóch rzeczywistych kontaktów tej samej osoby.