Idempotency w webhookach n8n: ta sama płatność tylko raz
Gdy Stripe wysyła ten sam webhook trzeci raz, Twoja architektura musi krzyczeć STOP. Webhook bez idempotency to nie automatyzacja, to loteria z ładnym UI.
Stripe potrafi wysłać ten sam webhook więcej niż raz. To nie bug. To normalka. Problem zaczyna się wtedy, gdy Twój workflow w n8n traktuje każde wejście jak nowe zdarzenie. Wtedy jeden klient dostaje dostęp dwa razy, faktura leci drugi raz, a księgowość robi minę jak recepcjonista, któremu ta sama osoba melduje się na ten sam pokój trzy razy.
W tym artykule rozkminimy, jak zrobić idempotency w webhookach n8n tak, żeby ta sama płatność nie została przetworzona dwa razy. Bez czarów, bez wielkiej architektury z kosmosu. Będzie klucz, tabela w bazie, logika workflow, edge case'y i przykład pod Stripe. Piszę to z perspektywy warsztatu procesu, bo brak idempotency wychodzi zawsze wtedy, gdy już jest ruch. Czyli gdy najmniej chcesz gasić pożar.
Czym jest idempotency w webhookach n8n?
Idempotency w webhookach n8n oznacza, że to samo zdarzenie może przyjść wiele razy, ale efekt biznesowy wykona się tylko raz.
Przykład prosty jak barszcz. Stripe wysyła event:
{
"id": "evt_123",
"type": "checkout.session.completed",
"data": {
"object": {
"id": "cs_test_456",
"payment_intent": "pi_789",
"customer_email": "klient@example.com"
}
}
}
Jeśli ten event przyjdzie trzy razy, workflow bez zabezpieczenia uruchomi się trzy razy:
- Tworzysz użytkownika trzy razy.
- Wysyłasz trzy maile powitalne.
- Nadajesz dostęp trzy razy.
- Robisz trzy wpisy w CRM.
- Księgowość zaczyna jojczeć, bo ma duble faktur.
Z idempotency pierwszy webhook przechodzi, a drugi i trzeci dostają grzeczne "to już było" i kończą bez efektów ubocznych.
Dlaczego Stripe wysyła ten sam webhook kilka razy?
Bo Stripe gwarantuje dostarczenie w modelu at-least-once, czyli "co najmniej raz". To świadoma decyzja projektowa, nie usterka. Lepiej dostarczyć event dwa razy niż zgubić go raz.
Ponowna wysyłka leci, gdy:
- Twój endpoint odpowiedział wolno i Stripe nie odebrał potwierdzenia w oknie czasowym.
- Workflow zwrócił błąd 5xx albo timeout.
- Odpowiedź 2xx zgubiła się gdzieś po drodze (sieć to nie magia).
Wniosek jest prosty: skoro nadawca może powtórzyć, to odbiorca musi umieć rozpoznać powtórkę. Ten obowiązek jest po Twojej stronie, nie po stronie Stripe.
Jaki klucz idempotency wybrać?
Klucz idempotency to coś, co jednoznacznie identyfikuje zdarzenie. Dwa rozsądne wybory:
Wariant prosty: identyfikator eventu Stripe, czyli pole id (np. evt_123). Stripe gwarantuje, że jest unikalny dla każdego zdarzenia. Dla jednego źródła płatności to wystarcza.
Wariant pod wielu dostawców: hash z połączenia źródła, identyfikatora i typu. Stosuję go, gdy webhooki sypią się z kilku systemów i nie chcę kolizji kluczy między nimi:
idempotency_key = sha256(provider + ":" + event_id + ":" + event_type)
// np. sha256("stripe:evt_123:checkout.session.completed")
Tabela w bazie i atomowy INSERT
Sednem idempotency jest jedna tabela z ograniczeniem UNIQUE na kluczu. To baza, nie n8n, pilnuje, żeby klucz wpadł tylko raz, nawet gdy dwa webhooki przyjdą w tej samej milisekundzie.
CREATE TABLE processed_events (
idempotency_key TEXT PRIMARY KEY,
status TEXT NOT NULL DEFAULT 'processing',
event_type TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
processed_at TIMESTAMPTZ
);
Trik polega na tym, żeby próba zapisu i sprawdzenie "czy już było" to była jedna, niepodzielna operacja:
INSERT INTO processed_events (idempotency_key, event_type)
VALUES ($1, $2)
ON CONFLICT (idempotency_key) DO NOTHING
RETURNING idempotency_key;
Jeśli zapytanie zwróci wiersz, to znaczy, że ten klucz wpadł teraz pierwszy raz, więc możesz przetwarzać. Jeśli nie zwróci nic, klucz już istniał, więc przerywasz. Bazodanowy UNIQUE rozwiązuje wyścig dwóch równoległych webhooków za Ciebie, bez kombinowania z blokadami w n8n.
Workflow w n8n krok po kroku
Tak układam ten przepływ w n8n:
- Webhook node przyjmuje POST od Stripe.
- Weryfikacja podpisu Stripe (
Stripe-Signature) i sprawdzenie świeżości timestampu, max kilka minut, żeby odbić ataki typu replay. - Function node buduje
idempotency_keyzideventu. - Postgres node robi
INSERT ... ON CONFLICT DO NOTHING RETURNING. - IF node: jeśli nic nie wróciło, kończ z odpowiedzią
200 "already processed". To kluczowy krok, idempotency siedzi na początku, nie na końcu. - Logika biznesowa: nadaj dostęp, wystaw fakturę, wyślij maila. Tylko teraz.
- Postgres node:
UPDATEstatusu nasuccessi zapisprocessed_at.
Idempotency to nie ostatni węzeł, który łapie duble. To pierwszy węzeł, który ich nie wpuszcza.
Statusy: processing, success, failed
Status w tabeli to nie ozdoba. To on decyduje, co robi kolejna powtórka eventu.
processingustawiasz w momencie zapisu klucza. Mówi: "ten event jest właśnie obrabiany".successpo zakończeniu logiki. Kolejna powtórka widzisuccessi grzecznie odpada.failedgdy logika padła. Tu masz decyzję: albo zostawiaszfaileddo ręcznego rozpatrzenia, albo kasujesz wiersz, żeby następna powtórka Stripe mogła spróbować od nowa.
processing a końcem logiki? Klucz zostaje na zawsze w processing i blokuje retry. Dlatego trzymam prostą regułę: event w processing starszy niż np. 15 minut traktuję jak porzucony i pozwalam go ponowić. Bez tego jedna padnięta egzekucja zamraża płatność na amen.
Najczęstsze błędy, które widzę
- Sprawdzanie duplikatu na końcu workflow. Wtedy efekty uboczne (mail, faktura) już poleciały. Idempotency musi być na pierwszym kroku.
- Klucz z danych biznesowych. Email albo kwota nie identyfikują zdarzenia. Trafiasz albo na fałszywe duble, albo na przepuszczone powtórki.
- Sprawdzanie SELECT-em, potem INSERT. Między jednym a drugim wciska się druga powtórka i masz dubel. Tylko atomowy
INSERT ... ON CONFLICT. - Brak weryfikacji podpisu Stripe. Idempotency chroni przed duplikatem, nie przed podrobionym requestem. To dwie różne sprawy, potrzebujesz obu.
Najczęstsze pytania
Czym jest idempotency w webhookach n8n?
To zasada, że to samo zdarzenie może przyjść wiele razy, ale efekt biznesowy wykona się tylko raz. Webhook na pierwszym kroku zapisuje klucz zdarzenia do tabeli z ograniczeniem UNIQUE i przerywa, gdy klucz już istnieje. Bez duplikatu płatności, faktury czy dostępu.
Dlaczego Stripe wysyła ten sam webhook kilka razy?
Bo działa w modelu at-least-once. Jeśli endpoint odpowie wolno, zwróci 5xx albo potwierdzenie 2xx zgubi się w sieci, Stripe ponowi wysyłkę. To projekt, nie błąd, więc to odbiorca musi rozpoznać powtórkę.
Jaki klucz idempotency wybrać dla Stripe?
Dla jednego źródła wystarczy identyfikator eventu Stripe (pole id, np. evt_123). Przy wielu dostawcach buduję klucz jako sha256 z połączenia źródła, identyfikatora i typu eventu, żeby uniknąć kolizji między systemami.
Masz webhooki, które dotykają płatności?
Zaczynam od warsztatu procesu (90 minut, bezpłatny). Rozrysujemy przepływ, znajdziemy miejsca, gdzie powtórka eventu robi dubla, i ustawimy idempotency tam, gdzie trzeba. Bez wciskania jednego słusznego stacku.
Zamów warsztat → Lub napisz bezpośrednio.