gdamian.pl
← Wszystkie wpisy
10 min

n8n webhook od zera: własne API, localhost, ngrok i zabezpieczenie HMAC 

Damian GębalaAutor

Webhook w n8n to node, który zamienia workflow w publiczne API: cokolwiek wyśle na ten adres, uruchamia Twój proces i może dostać odpowiedź z powrotem. Nie potrzebujesz do tego backendu ani osobnej aplikacji. Jedyna przeszkoda na starcie jest taka, że n8n musi być widoczne z internetu, a lokalnie siedzi na Twoim laptopie. Poniżej rozkładam to na cztery kawałki: dwa adresy webhooka, budowę własnego API, tunel przez ngrok i podpis HMAC, który weryfikujesz po swojej stronie.

Webhook w n8n jako własny endpoint API

Dokumentacja n8n opisuje webhook jako node wyzwalający: odbiera dane, gdy coś się wydarzy, i uruchamia workflow. Drugie zdanie tej definicji jest ważniejsze: to node pozwala zbudować własny endpoint API bez pisania backendu. Nie musisz stawiać aplikacji, wystarczą dwa node'y i adres, pod którym stoi n8n.

Node obsługuje sześć metod HTTP: DELETE, GET, HEAD, PATCH, POST i PUT. Uwaga na domyślne ustawienie nowego node'a: to GET. Większość integracji wysyła dane przez POST, a niezgodność metody potrafi dać komunikat o niezarejestrowanym webhooku, nawet jeśli adres jest poprawny.

Sama odpowiedź ma cztery tryby, ustawiane w sekcji Respond:

  • Immediately - zwraca kod i komunikat Workflow got started, bez czekania na resztę przepływu.
  • When Last Node Finishes - zwraca dane z ostatniego node'a, który się wykonał.
  • Using 'Respond to Webhook' Node - odpowiedź budujesz sam w osobnym node'u.
  • Streaming response - dane lecą do klienta na bieżąco, w trakcie przetwarzania.

Test czy Production: dwa adresy, jeden błąd

Node Webhook pokazuje dwa adresy, testowy i produkcyjny, i przełącza się między nimi w Params. To rozróżnienie jest numerem jeden wśród źródeł frustracji, bo oba wyglądają identycznie i różnią się jednym słowem w adresie.

Adres testowy rejestruje się po kliknięciu Listen for Test Event albo Execute workflow, o ile workflow nie jest aktywny. Jest tymczasowy: obsługuje jedno przyjście i sam się wyrejestrowuje. Jeden artykuł techniczny podaje, że timeout testowego listenera to około 120 sekund; dokumentacja n8n tego czasu nie opisuje, więc traktuj tę liczbę jako orientacyjną. Adres produkcyjny rejestruje się dopiero po publikacji workflow i działa, dopóki workflow jest aktywny. Dane z niego nie pojawiają się na żywo w edytorze - trzeba je podejrzeć w zakładce Executions.

Domyślnie path to losowy ciąg znaków, żeby nie kolidował z innymi webhookami. Możesz go zastąpić własnym, na przykład webhook/new-order zamiast webhook/893d-234s. Path przyjmuje też parametry trasy, między innymi /:variable/path/:variable2.

Budowa własnego API krok po kroku

Schemat jest prosty: Webhook na wejściu, w środku cokolwiek chcesz, na wyjściu Respond to Webhook. Jedna rzecz, o której łatwo zapomnieć: w sekcji Respond musisz jawnie ustawić tryb Using 'Respond to Webhook' node. Przy trybie Immediately n8n wysyła natychmiastowy 200 OK i node Respond to Webhook nigdy się nie wykona, nawet jeśli jest w grafie.

Node Respond to Webhook daje osiem typów odpowiedzi: All Incoming Items, First Incoming Item, JSON, Text, Binary File, JWT Token, Redirect i No Data. Kod HTTP ustawiasz w Response Code, a własne nagłówki w Response Headers. Jeśli zwracasz obiekt JavaScript w polu tekstowym, dostaniesz błąd Invalid JSON in 'Response body' - użyj {{ JSON.stringify($json.output) }}.

Druga pułapka jest podobna: panel wyjściowa Respond to Webhook domyślnie pokazuje to, co do node'a weszło, a nie to, co wyszło. Włącz Response Output Branch, a zobaczysz pełną odpowiedź wraz z nagłówkami i statusem. Wykonuje się tylko pierwszy napotkany node Respond to Webhook, więc przy rozgałęzieniach steruj przepływem warunkami.

Dane z żądania n8n parsuje na cztery klucze: headers, params, query i body. Czytasz je wyrażeniami, na przykład {{ $json.body.order.items[0].qty }}. Nagłówki z myślnikami wymagają nawiasów: {{ $json.headers['x-request-token'] }}. Notacja kropkowa na $json.headers.user-agent nie zadziała. Ciało w application/json n8n parsuje automatycznie do zagnieżdżonego obiektu.

Node HTTP Request działa w drugą stronę, czyli jako klient Twojego API. Ma siedem metod, tryby pól i JSON dla nagłówków i parametrów, pięć typów ciała oraz import gotowego żądania przez Import cURL. Dwie opcje warto ustawić świadomie: Never Error domyślnie uznaje żądanie za nieudane przy kodzie innym niż 2xx, a Timeout podaje się w milisekundach. Nie znalazłem w dokumentacji domyślnej wartości tego limitu, więc ustaw go sam, zamiast liczyć na szczęście.

Ten sam schemat, tylko w drugą stronę, wykorzystuję przy integracjach, gdzie klucz przychodzi z konta partnerskiego, na przykład przy wpinaniu e-faktur do procesu firmowego. Node HTTP Request obsługuje też paginację w trzech trybach, w tym przez wykrywanie linku do kolejnej strony w odpowiedzi, a w zmiennych wbudowanych masz $pageCount, $request i $response.

Lokalnie na laptopie: npx, Docker i port 5678

Najszybszy start to npx n8n, a potem http://localhost:5678. Polecenie pobiera wszystko, czego potrzeba, bez instalacji w systemie. Wymaganie to Node.js między 20.19 a 24.x. Instalacja przez npm install n8n -g działa, ale jest oznaczona jako przestarzała od n8n 3.0, więc traktuj ją jako przejściową.

Na dłuższszą metę wychodzi Docker. Wolumen trzyma dane między restartami:

docker volume create n8n_data
docker run -it --rm \
  --name n8n \
  -p 5678:5678 \
  -e GENERIC_TIMEZONE="Europe/Warsaw" \
  -e TZ="Europe/Warsaw" \
  -e N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true \
  -v n8n_data:/home/node/.n8n \
  n8nio/n8n

W chwili pisania dokumentacja podaje stabilną wersję 2.41.3 i beta 2.42.0. W przewodniku ngrok obraz nazywa się docker.n8n.io/n8nio/n8n, a w dokumentacji n8nio/n8n, to ten sam obraz z dwóch rejestrów. Zmienna N8N_RUNNERS_ENABLED jest przestarzała od n8n 2.0, mimo że wciąż widnieje w przykładowej komendzie, więc przy 2.x możesz ją pominąć.

Ngrok: wyciągnięcie localhostu na internet

Webhook z localhostu nie zadziała, bo usługa zewnętrzna nie ma jak do Ciebie dotrzeć. Najprostsze tego wyjaśnienie podaje OAuth: Google nie przyjmie adresu http://192.168.1.x:3000 jako redirect URI. Potrzebujesz publicznego adresu HTTPS, a ngrok go daje w kilka sekund.

Instalacja na Ubuntu to sudo snap install ngrok i ngrok config add-authtoken TWOJ_TOKEN. Sam tunel uruchamiasz komendą ngrok http 5678, wskazując port, na którym siedzi n8n. W sesji dostajesz publiczny adres w rodzaju https://abc123.ngrok-free.app; w przewodniku ngrok dla n8n ten sam mechanizm opisany jest domeną w rodzaju your-n8n-instance.ngrok.app.

Plan Free ma cztery istotne ograniczenia: jeden aktywny tunel, HTTPS bez własnych domen, losowy subdomen generowany przy każdym starcie sesji i 40 połączeń na minutę. Losowy adres to największa niedogodność: po restarcie musisz poprawiać adres w konfiguracji dostawcy. ngrok daje jeden statyczny adres na konto, rezerwowany w panelu, i wtedy URL przestaje się zmieniać. Plany płatne kosztują, według stanu na marzec 2026, około 10 dolarów miesięcznie, jeśli potrzebujesz domeny własnej i kilku tuneli naraz.

Dwie rzeczy, które ratują przy debugowaniu. Web Inspector pod 127.0.0.1:4040 pokazuje każde żądanie, które przeszło przez tunel, i pozwala odtworzyć je bez ponownego wywoływania zdarzenia. Po drugie, n8n ma wbudowany tunel na cloudflared, uruchamiany komendą pnpm stack --tunnel; sam wpisuje do pliku .env wartości WEBHOOK_URL i N8N_PROXY_HOPS=1. Dokumentacja mówi wprost, że to narzędzie do rozwoju lokalnego, nie do produkcji.

Zmienne środowiskowe, które robią całą robotę

Najczęstszy błąd przy przenoszeniu webhooka na produkcję to zostawienie adresu localhost w konfiguracji. Adres, pod którym n8n jest widoczny z zewnątrz, ustawia WEBHOOK_URL, a domenę instancji N8N_HOST. Przepis ngrok dla n8n wygląda tak:

NGROK_DOMAIN = twoja-instancja.ngrok.app
NGROK_AUTHTOKEN = twoj-token
N8N_HOST = twoja-instancja.ngrok.app
WEBHOOK_URL = "https://twoja-instancja.ngrok.app/"

Drugi limit, o którym trzeba wiedzieć wcześniej, to rozmiar ciała żądania. Oficjalna dokumentacja podaje dla webhooka limit 16 MB i zmienną N8N_PAYLOAD_SIZE_MAX, którą można podnieść. Starsze poradniki piszą o około 200 MB, powołując się na wewnętrzną bibliotekę parsera; traktuję tę drugą wartość jako nieaktualną i nie mieszam obu. Przy dużych plikach limit trzeba ustawić też na warstwie reverse proxy, inaczej plik odrzuci warstwa przed n8n.

Co n8n zabezpiecza sam, a czego nie

Tu warto być precyzyjny, bo temat bezpieczeństwa rodzi dużo mitów. Wbudowane metody uwierzytelniania webhooka to dokładnie cztery: Basic auth, Header auth, JWT auth i None. Header auth to statyczny token w nagłówku, w praktyce X-API-Key, i dla kontrolowanego środowiska bywa wystarczający. JWT wykorzystuje credential z kluczem typu Passphrase albo PEM Key. Basic auth jest najprostszy i najsłabszy, a None w produkcji nie ma sensu.

Obok tego node ma opcję IP(s) Allowlist, która odrzuca każde żądanie spoza listy kodem 403, oraz Ignore Bots, ignorujący linki podglądowe i crawlery. Jest też Only Run If: wyrażenie sprawdzane na żądaniu, a przy braku dopasowania klient dostaje 200 bez uruchamiania przepływu. Warto wiedzieć, że to wyrażenie działa po sprawdzeniu IP i uwierzytelnienia, ale jeśli samo się wywali, n8n zapisuje ostrzeżenie i przepuszcza żądanie dalej. Domyślnie dozwolone origins to *.

Tu kończy się lista wbudowanych możliwości, a zaczyna najczęstsze nieporozumienie przy webhookach: opcja IP Allowlist nie chroni przed atakiem, bo dostawca nie gwarantuje stałego zestawu adresów IP i może je zmienić bez ostrzeżenia.

HMAC: podpis, który weryfikujesz sam

Nie ma w n8n wbudowanej opcji Verify Signature: dokumentacja node'a Webhook i strona poświęcona credentialom nie wspominają o podpisywaniu żądań, a lista metod uwierzytelniania kończy się na None. Gotowy webhook z weryfikacją HMAC to funkcja dostawcy albo community node, nie platformy. Warto to powiedzieć wprost, bo tutoriale często mylą jedno z drugim.

Sam mechanizm wygląda tak. Nadawca liczy podpis HMAC-SHA256 z sekretu i złożonego ciągu zbudowanego ze znacznika czasu, nazw nagłówków, ich wartości i surowego body. Wynik dokleja do nagłówka w formacie t=..., h=..., v1=..., gdzie v1 to podpis zakodowany szesnastkowo. Odbiorca liczy tę samą wartość i porównuje.

Ten sam mechanizm w drugą stronę, gdy Twoje API wysyła podpisany request do usługi zewnętrznej, pojawia się przy integracji modelu językowego z procesem firmowym i warto go zaprojektować od razu.

Cztery zasady, bez których weryfikacja nie działa:

  • Podpisujesz surowe body, a nie obiekt po sparsowaniu. Ponowne serializowanie do JSON zmienia kolejność kluczy i spacje, więc podpis przestaje się zgadzać.
  • Porównujesz w czasie stałym, funkcją timingSafeEqual w Node.js albo hmac.compare_digest w Pythonie. Zwykłe porównanie stringów ujawnia podpis przez czas odpowiedzi.
  • Sprawdzasz znacznik czasu. Typowe okno tolerancji to 300 sekund; przycięte żądanie sprzed godziny albo z przyszłości oznacza odrzucenie. Niektóre integracje rozszerzają okno celowo, na przykład do 86400 sekund przy dostawach zaległych.
  • Rotujesz sekret bez utraty ruchu, akceptując przez chwilę podpisy złożone starym i nowym kluczem.

Praktyczny przykład z ekosystemu n8n: community node triggera webhooks, weryfikuje podpis każdego przyjścia automatycznie i odrzuca niepodpisane żądania, a sekret podpisu trzyma w credencialu, nie w parametrach node'a. Jeśli sekret się nie zgadza, node rzuca Invalid webhook signature; jeśli żądanie jest stare, pojawia się timestamp too old. Dokładnie takie komunikaty powinieneś zwracać także we własnym kodzie, zamiast ogólnego 500.

Jeśli dopiero zaczynasz z webhookami, zajrzyj najpierw do pierwszej automatyzacji w n8n krok po kroku, a sam n8n i powody, dla których firmy wybierają właśnie tę platformę warto mieć rozłożone zanim zaczniesz cokolwiek tunelować.

Pułapki, które kosztują godziny

Najczęstszy komunikat to {"code":404,"message":"The requested webhook is not registered."} i ma trzy typowe przyczyny. Pierwsza: strzelasz w adres testowy po pierwszym strzale albo po timeoutcie; trzeba kliknąć ponownie i dostać nowy listener. Druga: adres produkcyjny przy workflow, który jest zapisany, ale nieaktywny albo niezapisany. Trzecia, rzadziej spotykana: import albo duplikacja workflowu, po których rejestracja nie domyka się sama i trzeba usunąć problematyczny node Webhook, dodać nowy i zapisać ponownie.

Do tego dochodzi kilka rzeczy, o których dowiadujesz się dopiero w debuggerze. Node HTTP Request domyślnie obniża nazwy nagłówków do małych liter, więc $json.headers nie odpowiada temu, co widzisz w przeglądarce. Przekierowania są domyślnie śledzone. Od wersji 1.103.0 n8n zawija odpowiedzi HTML w piaskownicę iframe, więc kod sięgający do pamięci przeglądarki przestaje działać. A w przewodniku ngrok jest pułapka konfiguracyjna: jeśli najpierw ograniczysz ruch do swojego IP, a potem dodasz weryfikację webhooków, połączenia przestaną przechodzić, bo adresy dostawcy nie trafiły do allowlisty. Tam dokumentacja ngrok radzi wyłączyć ograniczenia na ścieżce webhooków.

Przy okazji: większość porażek w automatyzacjach zaczyna się od złego procesu, a nie od webhooków. Opisałem to w tekście o pięciu automatyzacjach, które się nie udały.

Na koniec dwie uwagi o kosztach i prywatności. Wersja społecznościowa n8n jest bezpłatna, licencja enterprise to już nie, a n8n samo odradza instalowanie na własnym serwerze tym, którzy nie zarządzają serwerami. Jeśli utrzymujesz n8n na własnym VPS, co zyskujesz na self-hostingu i gdzie są jego granice, opisałem osobno.

Napisz do mnie, jeśli chcesz to przełożyć na swoje procesy. Odpowiadam zwykle tego samego dnia, a zakres i wycenę dostajesz na piśmie, zanim cokolwiek zaczniemy.

Robisz coś podobnego u siebie?Napisz do mnie

Masz proces, który zjada tydzień pracy? Odzyskajmy ten tydzień.

Napisz do mnie