Webhook: co to jest, czym różni się od API i jak go odbierać, żeby nic nie zginęło
Webhook to żądanie HTTP POST, które inny system wysyła na Twój adres sam, gdy coś się wydarzy, na przykład bank potwierdzi płatność, klient złoży zamówienie albo zmieni się status subskrypcji. Od API różni się kierunkiem: API odpytujesz Ty, webhook przychodzi do Ciebie. Przewodnik jest dla właściciela sklepu lub firmy, który łączy płatności, sklep albo CRM z innymi programami, i dla programisty, który pisze odbiornik. Żeby nic nie zginęło, odbiornik sprawdza podpis nadawcy, odpowiada kodem 2xx, zanim zacznie dłuższą pracę, zapamiętuje identyfikatory zdarzeń, bo to samo zdarzenie potrafi przyjść dwa razy, nie zakłada kolejności i co jakiś czas uzgadnia stan przez API, bo według Shopify dostarczenie webhooka nie zawsze jest gwarantowane.
- Stripe, Shopify i GitHub podpisują webhooki kodem HMAC-SHA256 liczonym z surowej treści żądania, więc jej zmiana przez framework psuje weryfikację.
- Stripe każe odpowiedzieć kodem 2xx przed złożoną logiką, GitHub oczekuje odpowiedzi w ciągu 10 sekund, a Shopify w ciągu pięciu sekund na całe żądanie.
- Stripe ponawia niedostarczone zdarzenia w trybie produkcyjnym do trzech dni, Shopify 8 razy w ciągu 4 godzin, a GitHub nie ponawia nieudanych dostaw automatycznie; ręcznie ponowisz je do 3 dni wstecz.
- Stripe i Shopify nie gwarantują kolejności dostarczania zdarzeń i zastrzegają, że to samo zdarzenie może przyjść więcej niż raz.
Webhook a API: kto kogo woła
Oba mechanizmy przenoszą dane między systemami przez HTTP. Różnią się tym, kto zaczyna rozmowę i kiedy.
Przy zwykłym API to Ty pytasz: Twój program wysyła zapytanie do sklepu, bramki płatności albo CRM-u i dostaje odpowiedź. Żeby wiedzieć, czy jest nowe zamówienie, musisz pytać regularnie, także wtedy, gdy nic się nie zmieniło. To odpytywanie, po angielsku polling. Webhook odwraca kierunek: raz podajesz nadawcy adres i wybierasz zdarzenia, a on sam wysyła żądanie na ten adres, gdy zdarzenie zajdzie.
Dokumentacja Shopify pokazuje to na temacie orders/create: aplikacja subskrybuje temat i podaje adres, a gdy w sklepie powstaje zamówienie, Shopify wysyła tam nagłówki i dane zamówienia. Nie trzeba co chwilę pytać o zmiany.
GitHub wymienia trzy przewagi webhooków: mniej pracy i zasobów niż odpytywanie, lepsze skalowanie, bo przy wielu zasobach zapytania szybko wyczerpują limit API, i reakcja niemal w czasie rzeczywistym. Zastrzega jednak: jeśli dane są Ci potrzebne raz albo od czasu do czasu i z niewielu zasobów, wystarczy zapytać API.
Te mechanizmy się uzupełniają. Stripe radzi pobierać przez API obiekty, których brakuje po odebraniu zdarzenia, a Shopify mimo webhooków zaleca okresowe pobieranie danych przez API. Webhook mówi więc „coś się stało”, a API, jaki jest stan teraz.
Adres webhooka (ang. webhook URL albo endpoint) to adres HTTPS w Twoim systemie, który podajesz nadawcy, na przykład w panelu Stripe. Stripe pokazuje jego budowę: domena firmy i ścieżka przyjmująca żądania POST, czyli coś w rodzaju https://sklep.example.com/webhooks/stripe. Program, który pod tym adresem odbiera i sprawdza żądania, to odbiornik webhooków; o nim jest reszta przewodnika.
| Cecha | Odpytywanie API (polling) | Webhook |
|---|---|---|
| Kto zaczyna | Twój system pyta nadawcę | Nadawca sam wysyła żądanie na Twój adres |
| Kiedy dostajesz dane | Przy najbliższym zapytaniu według harmonogramu | Niemal od razu po zdarzeniu |
| Czego potrzebujesz | Klucza API i zadania uruchamianego cyklicznie | Publicznego adresu HTTPS i działającego odbiornika |
| Gdy Twój system nie działa | Zapytasz ponownie po powrocie | Nadawca ponawia dostawę albo nie, zależnie od swoich zasad |
| Kiedy lepszy | Dane potrzebne rzadko, z niewielu zasobów | Reakcja na zdarzenie: płatność, zamówienie, zmiana statusu |
Co przychodzi w żądaniu: treść i nagłówki
Stripe i Shopify opisują webhook jako zwykłe żądanie HTTP POST: treść w formacie JSON i kilka nagłówków, z których jeden niesie podpis nadawcy.
Treść różni się między nadawcami. W Stripe wybierasz format przy zakładaniu adresu: snapshot to pełny obiekt zdarzenia z identyfikatorem, typem i stanem obiektu z chwili zdarzenia, a thin to lekkie powiadomienie z identyfikatorem, typem i odnośnikiem do obiektu, którego aktualny stan pobierasz przez API. Dla nowych integracji Stripe zaleca format thin. Shopify domyślnie przysyła cały zasób, na przykład zamówienie, a temat zdarzenia podaje w nagłówku X-Shopify-Topic.
Ważniejsze są nagłówki. Jeden niesie podpis, dzięki któremu sprawdzisz, że żądanie wysłał nadawca. Identyfikator, po którym rozpoznasz powtórzoną dostawę, Shopify i GitHub też wysyłają w nagłówku, a Stripe w treści zdarzenia. Shopify dokłada czas wywołania w X-Shopify-Triggered-At, a GitHub typ zdarzenia w X-GitHub-Event.
Stripe przyjmuje wyłącznie publicznie dostępne adresy HTTPS, obsługuje tylko TLS 1.2 i 1.3 i pozwala zarejestrować do 16 miejsc docelowych zdarzeń (event destinations) na konto; do testów na własnym komputerze podsuwa Stripe CLI albo tymczasowy tunel. GitHub domyślnie weryfikuje certyfikat odbiornika i zaleca tego nie wyłączać, a Shopify sprawdza certyfikaty przy każdej dostawie.
Do adresu nie wpisuj sekretów. GitHub ostrzega przed umieszczaniem w nim kluczy API i innych danych uwierzytelniających i wskazuje podpis jako właściwy sposób sprawdzania nadawcy. Adres z tokenem łatwo trafia do logów serwera i historii konfiguracji, a podpis nie zdradza klucza, którym go policzono.
POST /webhooks/shopify HTTP/1.1
Host: aplikacja.example.com
Content-Type: application/json
X-Shopify-Topic: orders/create
X-Shopify-Hmac-Sha256: <HMAC-SHA256 surowej treści w base64>
X-Shopify-Shop-Domain: <sklep>.myshopify.com
X-Shopify-Webhook-Id: <identyfikator tej dostawy>
X-Shopify-Event-Id: <identyfikator zdarzenia>
X-Shopify-Triggered-At: <czas wywołania>
{
"id": <identyfikator zamówienia>,
"updated_at": "<data i czas ostatniej zmiany>",
"...": "pozostałe pola zamówienia"
}| Nadawca | Nagłówek z podpisem | Jak powstaje podpis | Identyfikator dostawy lub zdarzenia |
|---|---|---|---|
| Stripe | Stripe-Signature, w formacie t=znacznik czasu,v1=podpis | HMAC-SHA256 z ciągu „znacznik czasu, kropka, surowa treść”, liczony sekretem adresu zaczynającym się od whsec_ | Identyfikator zdarzenia evt_… w treści |
| Shopify | X-Shopify-Hmac-Sha256 | HMAC-SHA256 z surowej treści, liczony kluczem client secret aplikacji i zakodowany w base64 | X-Shopify-Webhook-Id dla pojedynczej dostawy; X-Shopify-Event-Id wspólny dla dostaw z jednej akcji w sklepie |
| GitHub | X-Hub-Signature-256, z przedrostkiem sha256= | HMAC-SHA256 z treści, liczony sekretem webhooka, zapisany szesnastkowo | X-GitHub-Delivery, taki sam także przy ręcznym ponowieniu |
Podpis i surowa treść żądania
Adres odbiornika jest publiczny, więc każdy może wysłać na niego dowolne dane. Stripe ostrzega, że bez weryfikacji fałszywe zdarzenie może wywołać realizację zamówienia, nadanie dostępu albo zmianę rekordów.
Stripe, Shopify i GitHub używają tego samego mechanizmu: HMAC z funkcją skrótu SHA-256. Nadawca i Ty znacie wspólny sekret; nadawca liczy z niego i z treści żądania podpis, a Twój odbiornik liczy go jeszcze raz i porównuje. Stripe dokłada do podpisywanego ciągu znacznik czasu i uznaje w trybie produkcyjnym tylko schemat v1, a pozostałe każe ignorować, żeby nikt nie zmusił odbiornika do słabszej metody. Starszy nagłówek GitHuba, liczony z SHA-1, jest wysyłany już tylko dla zgodności wstecznej.
Podpis liczy się z surowej treści, czyli dokładnie z bajtów, które przyszły w żądaniu. Stripe pisze, że każda jej zmiana psuje weryfikację, a frameworki potrafią dodać lub usunąć spacje, zmienić kolejność pól, zamienić tekst na obiekt JSON albo zmienić kodowanie. Przykład z dokumentacji to Express z express.json() przed trasą webhooka: treść zostaje sparsowana, zanim odbiornik sprawdzi podpis. Shopify radzi to samo: przechwyć surową treść i ustaw weryfikację przed middleware parsującym JSON.
Wynik porównujesz w stałym czasie. GitHub pisze wprost, żeby nie używać zwykłego operatora ==, tylko funkcji takich jak crypto.timingSafeEqual w Node.js, bo czas zwykłego porównania może zdradzić, jak długi fragment podpisu się zgadza.
Podpis nie chroni przed powtórzeniem przechwyconego żądania. Stripe rozwiązuje to podpisanym znacznikiem czasu: jego biblioteki domyślnie dopuszczają 5 minut różnicy, a dokumentacja ostrzega przed tolerancją 0, bo ta całkowicie wyłącza sprawdzanie świeżości. Zegar serwera synchronizuj przez NTP; ponowienia dostają nowy znacznik i podpis, więc tolerancji nie łamią. GitHub do tego samego celu podsuwa unikalny nagłówek X-GitHub-Delivery.
Sekret trzymasz poza kodem i wymieniasz okresowo albo od razu po podejrzeniu wycieku. Przy wymianie w Stripe stary sekret może działać jeszcze do 24 godzin, a nagłówek niesie wtedy po jednym podpisie v1 dla każdego aktywnego sekretu. W Shopify przejście podpisów na nowy client secret może potrwać do godziny.
Stripe zaleca łączyć podpis z listą dozwolonych adresów IP nadawcy; PayU i GitHub też publikują swoje adresy, a GitHub uprzedza, że czasem je zmienia. Jeśli framework wymaga tokenu CSRF przy każdym żądaniu POST, trasę webhooka trzeba z tego wyłączyć, bo nadawca takiego tokenu nie wyśle.
import { createHmac, timingSafeEqual } from 'node:crypto';
// rawBody: bajty żądania dokładnie tak, jak przyszły, przed JSON.parse
export function verifyStripeSignature(
rawBody: Buffer,
header: string,
secret: string,
toleranceSeconds = 300,
): boolean {
const pairs = header.split(',').map((part) => part.split('='));
const timestamp = pairs.find(([key]) => key === 't')?.[1];
const signatures = pairs.filter(([key]) => key === 'v1').map(([, value]) => value ?? '');
if (!timestamp || signatures.length === 0) return false;
const ageSeconds = Date.now() / 1000 - Number(timestamp);
if (!Number.isFinite(ageSeconds) || Math.abs(ageSeconds) > toleranceSeconds) return false;
const expected = createHmac('sha256', secret)
.update(`${timestamp}.`)
.update(rawBody)
.digest();
// Po wymianie sekretu nagłówek może nieść kilka podpisów v1.
return signatures.some((signature) => {
const received = Buffer.from(signature, 'hex');
return received.length === expected.length && timingSafeEqual(received, expected);
});
}Najpierw 2xx, potem kolejka
Nadawcy czekają na odpowiedź krótko, a spóźniona odpowiedź to dla nich nieudana dostawa: Stripe i Shopify wyślą wtedy zdarzenie jeszcze raz, GitHub nie.
Stripe każe zwrócić kod 2xx, zanim zacznie się złożona logika, która mogłaby przekroczyć czas: odpowiedź 200 ma paść, zanim oznaczysz fakturę jako opłaconą w systemie księgowym. GitHub oczekuje odpowiedzi 2XX w ciągu 10 sekund, potem zrywa połączenie. Shopify daje sekundę na nawiązanie połączenia i pięć sekund na całe żądanie.
Dlatego odbiornik robi tylko trzy rzeczy: sprawdza podpis, zapisuje zdarzenie i odpowiada. Maile, dokumenty i magazyn obsługuje osobny proces, który pobiera zdarzenia z kolejki. Stripe zaleca taką kolejkę także z innego powodu: przy obsłudze synchronicznej nagły skok liczby zdarzeń, na przykład na początku miesiąca, gdy odnawiają się subskrypcje, może przeciążyć serwer.
Kolejność kroków ma znaczenie. Najpierw podpis, bo Stripe każe sprawdzać zdarzenie, zanim cokolwiek na jego podstawie zrobisz. Potem trwały zapis zdarzenia z identyfikatorem, bo Stripe i Shopify każą rozpoznawać duplikaty po zapisanych identyfikatorach, i dopiero po nim kod 2xx. Jeśli zapis się nie uda, bo baza chwilowo nie działa, zwróć 5xx, a nie 2xx, żeby nadawca nie uznał za dostarczone zdarzenia, którego nikt nie zapamiętał. Stripe, Shopify i PayU spróbują wtedy ponownie same, a GitHub nie ponawia nieudanych dostaw, więc tam ponowisz dostawę ręcznie.
Przekierowanie to też błąd. Stripe traktuje odpowiedzi 3xx jako nieudane dostawy i każe wpisać adres, na który przekierowanie prowadzi, a Shopify każdą odpowiedź spoza zakresu 200, łącznie z 3XX, uznaje za błąd. Po przejściu na HTTPS, zmianie domeny albo samej konwencji ukośnika na końcu adresu sprawdź więc adres webhooka w panelu nadawcy. Mapę przekierowań i wybór jednej formy adresu opisuje przewodnik o przebudowie strony bez utraty ruchu.
W tabeli błędów Stripe kody 401, 403 i 405 oznaczają ograniczenia dostępu do adresu, a 404 adres, którego nie ma; Stripe radzi wtedy sprawdzić, czy adres jest publiczny i przyjmuje metodę POST. Hasło na wersji testowej albo zapora blokująca nieznane żądania może więc zatrzymać webhooki, zanim dotrą do aplikacji i jej logów.
Ustaw subskrypcję tylko na te typy zdarzeń, których integracja używa. Stripe odradza nasłuch wszystkich zdarzeń, bo niepotrzebnie obciąża serwer, a GitHub radzi przed obsługą sprawdzić typ zdarzenia i akcję, bo nowe pojawiają się z czasem.
Ponowienia, duplikaty i kolejność zdarzeń
Ponowienia ratują zdarzenia, gdy odbiornik chwilowo nie działa, ale Stripe i Shopify zastrzegają, że to samo zdarzenie może przyjść więcej niż raz i nie w tej kolejności, w jakiej powstało.
Każdy nadawca ponawia inaczej, a jeden wcale. Stripe próbuje dostarczyć zdarzenie w trybie produkcyjnym do trzech dni, z coraz dłuższymi odstępami, ale w środowisku testowym (sandbox) tylko trzy razy w ciągu kilku godzin, więc test w sandboksie nie pokaże skutków dłuższej awarii. Ręcznie ponowisz zdarzenie w panelu do 15 dni od jego utworzenia, a przez Stripe CLI do 30 dni. Shopify ponawia 8 razy w ciągu 4 godzin, a po 8 nieudanych próbach z rzędu usuwa subskrypcję utworzoną przez Admin API. GitHub nie ponawia nieudanych dostaw automatycznie; ręcznie, w panelu albo przez REST API, ponowisz dostawy z ostatnich 3 dni.
Czas na naprawę odbiornika zależy więc od nadawcy: po weekendowej awarii zdarzenia ze Stripe mają jeszcze szansę dotrzeć, subskrypcji w Shopify może już nie być, a z GitHuba nic nie przyjdzie, dopóki sam nie ponowisz dostaw. Operatorzy płatności mają własne zasady, na przykład PayU oczekuje kodu 200, a nie dowolnego 2xx, a ich harmonogramy ponowień i sposób potwierdzania płatności zestawia przewodnik jak wybrać bramkę płatności.
Duplikaty to normalna część działania webhooków. Stripe zaleca zapisywać identyfikatory obsłużonych zdarzeń i pomijać te, które już są w rejestrze; gdy Stripe wygeneruje dwa osobne zdarzenia, duplikat rozpoznasz po identyfikatorze obiektu z data.object razem z typem zdarzenia. Shopify zaleca operacje idempotentne, czyli takie, których dwukrotne wykonanie daje ten sam wynik, a gdy to niemożliwe, pomijanie dostaw z zapisanym już X-Shopify-Webhook-Id.
Kolejności nie gwarantuje ani Stripe, ani Shopify. Przy tworzeniu subskrypcji w Stripe zdarzenia customer.subscription.created, invoice.created i invoice.paid nie muszą przyjść w tym porządku. W zdarzeniach snapshot pole created ma dokładność do sekundy, więc dwa różne zdarzenia mogą mieć ten sam znacznik; Stripe każe nie używać go ani do ustalania kolejności, ani do wykrywania duplikatów. W Shopify products/update może przyjść przed products/create, a do porządkowania zdarzeń Shopify poleca nagłówek X-Shopify-Triggered-At albo pole updated_at.
Stąd dwie zasady dla kodu. Stan zmienia się tylko do przodu: opłacone zamówienie nie wraca do oczekującego tylko dlatego, że starsze zdarzenie przyszło później. W Shopify porównasz updated_at, a gdy nadawca nie podaje czasu zmiany obiektu, pobierz jego aktualny stan przez API. Gdy zdarzenie przychodzi za wcześnie, na przykład invoice.paid przed subskrypcją, też pobierz brakujące obiekty przez API; to przykład samego Stripe.
Tabela niżej łączy wykrywanie duplikatów z kolejką. W PostgreSQL klauzula RETURNING zwraca tylko wiersze naprawdę wstawione, więc pusty wynik po ON CONFLICT DO NOTHING oznacza duplikat.
CREATE TABLE webhook_events (
source text NOT NULL, -- 'stripe', 'shopify', 'github'
event_id text NOT NULL, -- evt_…, X-Shopify-Webhook-Id, X-GitHub-Delivery
payload jsonb NOT NULL,
received_at timestamptz NOT NULL DEFAULT now(),
processed_at timestamptz, -- NULL: czeka na obsługę w tle
PRIMARY KEY (source, event_id)
);
-- W odbiorniku, po sprawdzeniu podpisu, przed odpowiedzią:
INSERT INTO webhook_events (source, event_id, payload)
VALUES ($1, $2, $3)
ON CONFLICT (source, event_id) DO NOTHING
RETURNING event_id; -- zero wierszy: duplikat, odpowiedz 2xx i nic więcej nie róbUzgadnianie stanu przez API
Ponowienia nie dają pewności, że każde zdarzenie dotrze. Shopify pisze wprost, że aplikacja nie powinna polegać na samych webhookach, i zaleca okresowe pobieranie danych przez API.
Według Shopify dostarczenie webhooka nie zawsze jest gwarantowane, a aplikacja może też pominąć zdarzenie przez własny błąd albo przerwę w działaniu. Zadania uzgadniające (reconciliation jobs) pobierają więc dane cyklicznie, a ponieważ wiele zapytań GraphQL Shopify pozwala filtrować po updated_at, każde uruchomienie pobiera tylko obiekty zmienione od poprzedniego.
W Stripe lista zdarzeń w API z parametrem delivery_success=false zwraca te, których nie udało się dostarczyć, a parametr ending_before pozwala zacząć od ostatniego zdarzenia sprzed awarii; obejmuje to tylko ostatnie 30 dni. Zdarzenia obsłużone ręcznie Stripe dalej ponawia, więc odbiornik ma je pominąć i odpowiedzieć sukcesem; wtedy ponowienia ustaną.
Częstotliwość uzgadniania dobierz do kosztu przeoczonego zdarzenia: nieprzyznany dostęp po zapłacie wymaga krótszego cyklu niż synchronizacja opisów produktów. Każdą rozbieżność popraw i zapisz, bo pokazuje, gdzie ginie zdarzenie.
Uzgadnianie nie zastąpi monitoringu. W Stripe zakładka Event deliveries w Workbench pokazuje dostarczone, oczekujące i nieudane zdarzenia z kodem odpowiedzi i terminem kolejnej próby. Shopify wysyła ostrzeżenia na awaryjny adres e-mail dewelopera aplikacji, więc ten adres musi prowadzić do skrzynki, którą ktoś czyta.
Odbiór w n8n, Make i Zapierze
Narzędzia no-code tworzą adres webhooka bez pisania kodu, ale wymagania nadawcy wobec odbiornika zostają te same.
W n8n węzeł Webhook, a w Make moduł webhooka tworzą adres, który wklejasz do panelu nadawcy albo do formularza; na tej samej zasadzie działają wyzwalacze natychmiastowe w Zapierze. Które narzędzie wybrać, rozstrzyga porównanie n8n, Make i Zapier.
Największa pułapka to znaczenie odpowiedzi. Make zapisuje przyjęte żądanie w kolejce webhooka i, jeśli scenariusz nie ma modułu Webhook response, odpowiada kodem 200 z treścią Accepted, czyli potwierdza przyjęcie do kolejki, a nie obsługę. Przy pełnej kolejce Make odrzuca żądanie kodem 400, a powyżej 300 żądań w ciągu 10 sekund odpowiada kodem 429. Węzeł Webhook w n8n w trybie Immediately odpowiada komunikatem Workflow got started, czyli potwierdza start przepływu, a nie zapis u celu. Szczegóły i różnicę między adresem testowym a produkcyjnym w n8n opisuje sekcja pośrednik: n8n, Make, Zapier i co znaczy odpowiedź 200.
Druga pułapka to podpis. Dokumentacja węzła Webhook w n8n wymienia uwierzytelnianie Basic, nagłówkiem i JWT, a sprawdzania podpisu HMAC nadawcy na tej liście nie ma. Jeśli zdarzenia ze Stripe czy Shopify odbierasz takim ogólnym węzłem, a nie gotową integracją nadawcy, weryfikację na surowej treści musisz dodać sam.
Lista kontrolna odbiornika
Osiem punktów do sprawdzenia przed uruchomieniem integracji i po każdej zmianie adresu, serwera albo frameworka.
Jeśli płatności obsługuje gotowa wtyczka sklepu, odbiornik należy do wtyczki; Tobie zostaje pilnowanie adresu webhooka przy przebudowie strony i czytanie powiadomień o nieudanych dostawach.
Własny odbiornik ma sens, gdy zdarzenie ma uruchomić coś, czego wtyczki nie robią: nadać dostęp w aplikacji, przekazać dane do księgowości albo spiąć kilka systemów w jeden proces. To zakres usługi integracje API, a lista poniżej to minimum, którego warto wymagać od każdego wykonawcy, także ode mnie.
- Adres docelowyAdres jest publiczny, działa po HTTPS z ważnym certyfikatem, przyjmuje metodę POST, nie przekierowuje i nie zawiera sekretów. Po każdej przebudowie strony sprawdź go jeszcze raz w panelu nadawcy.
- Podpis na surowej treściOdbiornik sprawdza podpis, zanim sparsuje JSON, porównuje go w stałym czasie, a sekret trzyma poza kodem. Prosty test: zmień jeden znak w treści i sprawdź, czy żądanie zostanie odrzucone.
- ŚwieżośćJeśli nadawca podpisuje znacznik czasu, odbiornik odrzuca zbyt stare żądania. Biblioteki Stripe domyślnie dopuszczają 5 minut różnicy; tolerancji 0 nie ustawiaj, bo wyłącza sprawdzanie. Zegar serwera synchronizuje NTP.
- Trwały zapis zdarzeniaIdentyfikator zdarzenia lub dostawy trafia do kolumny z ograniczeniem unikalności, zanim padnie odpowiedź. Duplikat dostaje odpowiedź 2xx i nie jest obsługiwany drugi raz.
- Szybka odpowiedźOdbiornik zwraca kod, którego oczekuje nadawca, i mieści się w jego limicie czasu: Shopify daje pięć sekund na całe żądanie, a GitHub dziesięć. Maile, dokumenty i zmiany w magazynie obsługuje osobny proces w tle.
- Stan zmienia się tylko do przoduKod nie zakłada, że zdarzenia przyjdą po kolei. Zanim nadpisze rekord, porównuje czas zmiany obiektu, w Shopify pole updated_at, a gdy danych brakuje, pobiera obiekt przez API.
- UzgadnianieZadanie uruchamiane cyklicznie porównuje stan u Ciebie ze stanem w API nadawcy, poprawia różnice i zapisuje każdą z nich, żeby było widać, gdzie giną zdarzenia.
- Monitoring i test awariiAlarm przychodzi zarówno przy błędach odbiornika, jak i wtedy, gdy zdarzenia przestają napływać, a ostrzeżenia nadawcy trafiają do skrzynki, którą ktoś czyta. W środowisku testowym wyłącz odbiornik na kilka minut i sprawdź, czy ponowienia albo uzgadnianie domkną zaległe zdarzenia.
Pytania i odpowiedzi
Do czego służy webhook?
Do powiadamiania innego systemu o zdarzeniu w chwili, gdy się wydarzy, bez ciągłego odpytywania. Typowe przykłady to potwierdzenie płatności, nowe zamówienie, zmiana statusu subskrypcji albo zgłoszenie z formularza. Dokumentacja Shopify wymienia też powiadomienia o stanach magazynowych i przekazywanie danych zamówień do oprogramowania księgowego.
Czym webhook różni się od API?
Kierunkiem. Przy API Twój system pyta i dostaje odpowiedź; przy webhooku nadawca sam wysyła żądanie HTTP na Twój adres, gdy coś się zmieni. Webhook nie zastępuje API: mówi, że coś się stało, a API pozwala sprawdzić aktualny stan, pobrać brakujące dane i okresowo uzgodnić, czy żadne zdarzenie nie zginęło.
Co to jest adres webhooka (webhook URL)?
To adres HTTPS w Twoim systemie, który podajesz nadawcy, na przykład w panelu Stripe albo w konfiguracji aplikacji Shopify, i na który nadawca wysyła zdarzenia. Musi być publicznie dostępny, przyjmować żądania POST i odpowiadać bez przekierowań, bo Stripe i Shopify traktują odpowiedzi 3xx jako nieudane dostawy. Nie umieszczaj w nim kluczy ani haseł; do sprawdzania nadawcy służy podpis.
Co zrobić, gdy webhook nie dochodzi?
Zacznij od historii dostaw u nadawcy; w Stripe zakładka Event deliveries pokazuje kod odpowiedzi każdej próby. Kod 3xx to przekierowanie, na przykład po zmianie domeny; 401, 403 lub 405 to ograniczenie dostępu; 404 to nieistniejący adres; przekroczenie czasu to zbyt długa praca przed odpowiedzią. Po naprawie ponów zaległe zdarzenia: w Stripe z panelu do 15 dni albo przez Stripe CLI do 30 dni, w GitHubie ręcznie, dla dostaw z ostatnich 3 dni, bo sam nie ponawia. W Shopify sprawdź, czy subskrypcja nadal istnieje.
Czy webhook jest bezpieczny?
Tak, jeśli odbiornik sprawdza podpis. Adres webhooka jest publiczny, więc bez weryfikacji każdy może wysłać na niego fałszywe zdarzenie, na przykład o opłaconym zamówieniu. Stripe, Shopify i GitHub podpisują dostawy kodem HMAC-SHA256, który sprawdzasz na surowej treści i porównujesz w stałym czasie. Do tego HTTPS, sekret poza kodem i adresem oraz tolerancja znacznika czasu przeciw powtórzeniom.
Dlaczego ten sam webhook przyszedł dwa razy?
Bo nadawca ponowił dostawę, na przykład gdy odpowiedź nie dotarła na czas albo połączenie zostało zerwane. Stripe i Shopify piszą wprost, że to samo zdarzenie może przyjść więcej niż raz. Zapisuj identyfikator zdarzenia (w Stripe evt_…, w Shopify nagłówek X-Shopify-Webhook-Id) w kolumnie z ograniczeniem unikalności, a drugą dostawę kończ odpowiedzią 2xx bez ponownej obsługi.
Jaki kod odpowiedzi ma zwrócić odbiornik webhooka?
Kod sukcesu z rodziny 2xx, zwrócony od razu po sprawdzeniu podpisu i zapisaniu zdarzenia, a przed dłuższą pracą. Stripe przyjmuje dowolny kod 2xx, Shopify odpowiedź z zakresu 200 w ciągu pięciu sekund, GitHub odpowiedź 2XX w ciągu 10 sekund, a PayU oczekuje dokładnie kodu 200. Jeśli zapis zdarzenia się nie udał, zwróć błąd 5xx, żeby Stripe, Shopify czy PayU spróbowały ponownie; GitHub nie ponawia nieudanych dostaw sam, więc tam dostawę ponawiasz ręcznie.
Źródła
Liczby i terminy w tym przewodniku sprawdziłem w oficjalnych źródłach. Ceny i przepisy się zmieniają, więc przy decyzji warto zajrzeć do nich ponownie.
- Stripe: odbieranie zdarzeń na adresie webhooka (Receive Stripe events in your webhook endpoint)
- Stripe: błędy weryfikacji podpisu, surowa treść żądania i Express (Manage webhook endpoints)
- Stripe: przetwarzanie niedostarczonych zdarzeń (Process undelivered events)
- Stripe: jak działają zdarzenia, formaty thin i snapshot, limit 16 miejsc docelowych (How events work)
- Shopify: webhooki, kolejność zdarzeń i zadania uzgadniające (About webhooks)
- Shopify: weryfikacja dostaw, duplikaty, limity czasu i ponowienia (Verify webhook deliveries)
- Shopify: struktura dostawy i nagłówki (Webhooks delivery structure)
- GitHub Docs: czym są webhooki i kiedy wybrać API (About webhooks)
- GitHub Docs: dobre praktyki korzystania z webhooków
- GitHub Docs: weryfikacja dostaw webhooków
- GitHub Docs: ponawianie dostaw webhooków
- PayU: cykl życia płatności i powiadomienia (Payment lifecycle)
- Make: webhooki i kolejka webhooka
- n8n: węzeł Webhook, adresy testowy i produkcyjny, tryby odpowiedzi
- PostgreSQL: INSERT, ON CONFLICT i RETURNING
Masz pytanie do swojego projektu?
Opisz, co chcesz osiągnąć. Odpowiem konkretnie, także wtedy, gdy najlepszą odpowiedzią jest „nie rób tego”.
Jonasz Jankowski · Bezpośrednia współpraca · Mikołów (Śląsk), zdalnie w całej Polsce
- Odpowiadam osobiście w ciągu jednego dnia roboczego.
- Krótka rozmowa o celu, funkcjach, materiałach i integracjach.
- Dostajesz zakres z etapami i konkretną kwotę, zanim cokolwiek zaczniemy.