Webhooks are how you turn n8n workflows into callable endpoints. In this guide, we’ll cover what they are, how to test vs. run in production, the main HTTP methods (GET/POST/PATCH/DELETE/PUT/HEAD), returning responses, locking things down with authentication (Basic, Header/API-Key/Bearer, JWT), and hardening options like CORS, IP allowlists, and more.
If you’re following along with the video, you can recreate the demo using Supabase for a simple contacts table and Postman/cURL for requests. A SQL placeholder is included below—paste your script there when ready.
What is a webhook in n8n?
An n8n Webhook node exposes an HTTP endpoint that can receive data (query params, headers, body including JSON/form/binary) and trigger a workflow. It can also return data either immediately or after other nodes finish—effectively letting you build lightweight API endpoints.
Two URLs per webhook:
- Test URL (for development) — use while building; receive data directly in the Editor UI. It’s active only while you’re listening, and listens for ~120 seconds.
- Production URL (for live use) — requires the workflow to be Active (toggled on).
Test vs Production: how they behave
- Test URL only triggers the Webhook node; it’s great for capturing sample payloads while you map fields downstream. Click Listen for test event to make it live.
- Production URL runs the whole workflow and can return data depending on your response settings (explained below).
Tip: If you self-host behind a reverse proxy or load balancer, ensure your Production URL is correctly exposed end-to-end (HTTPS recommended). If you switch between URLs and see odd behaviour, double-check your environment and any reported bugs for your n8n version.
HTTP methods you can use
n8n’s Webhook supports: GET, POST, PATCH, DELETE, PUT, HEAD. Use them as you would in a typical REST flow (GET for reads, POST for creation,
PATCH for partial update, PUT for full replace, DELETE for removal, HEAD for headers-only checks).
Example: GET with query parameters
Say we want to fetch a contact by email.
Request (cURL)
curl -G "https://your-n8n-domain/webhook/contacts" \
--data-urlencode "email=charlie@example.com"
In n8n:
- Use the Webhook (GET) → capture
{{$json.query.email}}
. - Feed it to your Supabase node’s filter.
- For testing, use the Test URL while listening; then switch to Production URL when ready.
Example: POST to create a row (JSON body)
Your incoming body typically looks like:
{ "name": "John Free", "email": "john.free@example.com" }
In n8n:
- Webhook (POST) receives that under
{{$json.body}}
. - Use a Set node to flatten the structure (map
body.name
→name
,body.email
→email
) so your Supabase node can auto-map input data to columns as expected. - Create the row and return the new ID.
PATCH vs PUT (important distinction)
- PATCH = partial update of fields.
- PUT = full replacement of the resource (send all fields).
(Don’t use PATCH and describe it as PUT—keep the semantics clean.)
DELETE with immediate response
If you don’t need to send a body back, set the Webhook to respond immediately with a status (e.g., 200) and perform the deletion downstream.
Returning data to the caller
n8n gives you three main response modes:
- Immediately — returns
200
with a simple message (no waiting for downstream nodes). - When Last Node Finishes — returns the output of the last executed node.
- Using Respond to Webhook — lets you decide exactly when/what to respond with (anywhere in the workflow).
Respond to Webhook tips:
- Only the first Respond to Webhook executes if there are multiple.
- If the workflow finishes without hitting a Respond to Webhook, n8n returns a standard
200
. - You can enable a secondary “response output” branch to capture and log what was sent.
Need to return arrays? Either aggregate to a single item before Respond to Webhook, or use When Last Node Finishes to send the last node’s full output.
Security: never leave webhooks open
At a minimum, use one of the built-in methods below. (Pair with HTTPS. Consider rate limiting and replay protection if handling anything sensitive or costly.)
1) Basic Auth
Username + password on the webhook. Simple, but keep the credentials safe.
2) Header Auth (API-Key/Bearer)
Send a header like:
Authorization: Bearer <token>
or a custom header (e.g. x-api-key: <value>
). This is widely used and easy to implement.
3) JWT (JSON Web Token)
n8n can validate JWT signatures using your configured secret/key + algorithm. This confirms the token is genuine. Important: claims such as exp
, iss
, aud
are not automatically enforced unless you explicitly check them in the workflow (e.g., Code node or dedicated checks). Don’t assume expiry/roles are validated for you—add that logic if you need it.
Where do you get the JWT? In many apps (e.g. Supabase), the frontend authenticates, receives an access token, and you forward it as a Bearer token to your webhook. Your webhook then verifies the signature and inspects claims before proceeding.
Hardening options (beyond auth)
- CORS Allowed Origins: restrict which front-end origins can hit your webhook (default
*
). Set a comma-separated allow-list (e.g.https://app.example.com
). - Ignore Bots: drop requests from link previewers/web crawlers. Handy for public links.
- IP allowlist: restrict which source IPs can call your webhook. If you’re behind a reverse proxy, set
N8N_PROXY_HOPS
so n8n sees the real client IP (or the correct proxy header in the chain).
Self-hosting note: Historically this was done at your reverse proxy (nginx/Caddy). Newer n8n builds include an IP whitelist option in webhook call settings; if requests aren’t matching, check proxy hop headers and your hop count.
Extra patterns to consider (DIY):
- Replay protection: require a timestamp + signature (HMAC) and reject old timestamps.
- Rate limiting: simple in-workflow throttling or a proxy/CDN policy to stop floods.
- Payload size limits: default webhook max is ~16 MB (configurable via
N8N_PAYLOAD_SIZE_MAX
if self-hosting).
Streaming responses (AI/chat UX)
n8n supports streamed responses for real-time UIs (e.g., AI chat). Use with care, set headers appropriately (e.g., text/event-stream
) and be mindful of client expectations and timeouts. (Behaviour and header control can vary; check current docs/releases and test your client.)
Common gotchas (and quick fixes)
- “I hit the Test URL but nothing happens” → You must click Listen for test event first; it times out after ~120s.
- Multiple Respond to Webhook nodes → only the first one takes effect.
- CORS preflights (OPTIONS/HEAD) → configure Allowed Origins and return sensible headers.
- IP allowlist not working behind proxy → set
N8N_PROXY_HOPS
to match your chain. - File too large → mind the ~16 MB payload limit (or raise it when self-hosting).
Supabase: SQL (demo contacts)
Use this SQL to create the demo table the video uses (e.g.demo_contacts
withid
,name
,
-- Table: public.demo_contacts
-- Drop if exists (safe reset for repeat runs)
DROP TABLE IF EXISTS public.demo_contacts;
-- Create table
CREATE TABLE public.demo_contacts (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
phone TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);
-- Function to auto-update "updated_at"
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ language 'plpgsql';
-- Trigger for auto-updating "updated_at"
DROP TRIGGER IF EXISTS set_updated_at ON public.demo_contacts;
CREATE TRIGGER set_updated_at
BEFORE UPDATE ON public.demo_contacts
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- Insert some demo data
INSERT INTO public.demo_contacts (name, email, phone)
VALUES
('Alice Johnson', 'alice@example.com', '555-1234'),
('Bob Smith', 'bob@example.com', '555-5678'),
('Charlie Lee', 'charlie@example.com', '555-9012');
Step-by-step: putting it together
Use the n8n tempalte or follow along below: https://n8n.io/workflows/8258-learn-secure-webhook-apis-with-authentication-and-supabase-integration/
- Create the Webhook
- Start with GET (Test URL), click Listen, send a request with
?email=...
, pin the sample.
- Query Supabase
- Add a Supabase node → Get row(s) using
email
. Map{{$json.query.email}}
.
- Return a response
- For testing: When Last Node Finishes or add Respond to Webhook with the JSON you want.
- POST create
- Switch Webhook to POST; body JSON arrives under
{{$json.body}}
. - Set node to flatten → Supabase Insert (auto-map to columns). Return the new ID.
- PATCH update
- Webhook PATCH; pass
id
+ fields to update. Supabase Update with filter onid
. - Keep PATCH (partial) distinct from PUT (full replace).
- DELETE
- Webhook DELETE; filter by
id
. - Respond Immediately (status only) if you don’t need a body.
- Lock it down
- Pick Basic, Header/Bearer, or JWT (and validate any required claims yourself).
- Set Allowed Origins (CORS), consider IP allowlist, ignore bots, and add rate/replay protection.
Final checklist
- Using Production URL with the workflow Active
- Response mode set (Immediate / Last Node / Respond to Webhook) as needed
- Auth enabled (Basic / Header / JWT) and tested with a real client
- CORS origins restricted; Ignore bots on for public endpoints
- (If used) IP allowlist verified behind proxies with
N8N_PROXY_HOPS
- Payload sizes within limit (or configured)
Wrap-up
That’s the core of n8n webhooks: how to send/receive data, return results, and—critically, secure the endpoint. With a few small settings (auth, CORS, allowlists) and sensible patterns (rate/replay protection), your webhook endpoints stay reliable and safe.
If you’re following the video, drop your Supabase SQL into the placeholder above, run it, and you’re ready to experiment.
