n8n Webhooks Explained: A Practical Guide
Master n8n webhooks: test vs production URLs, HTTP methods, auth, reading payloads, responding to callers, security, and a debugging checklist.
On this page
- What is a webhook (and why not just poll)?
- How the n8n Webhook node works
- Test URL vs production URL: the 1 gotcha
- HTTP methods, paths, and authentication
- Methods
- Paths
- Authentication
- Reading the incoming data
- Responding to the caller
- Real-world examples
- Security hardening
- When a webhook isn't firing: debugging checklist
- Limitations to keep in mind
- Keep learning
Webhooks are the single most useful trigger in n8n, and also the one beginners get stuck on most often. Once they click, you can fire a workflow the instant something happens in almost any app: a form submission, a payment, a new commit, a chat message. This guide goes deep on how the Webhook node actually works, the gotchas that waste hours, and how to ship one safely.
If you are brand new to the platform, start with what is n8n and the beginner's guide, then come back here.
What is a webhook (and why not just poll)?
A webhook is a URL that listens for incoming HTTP requests. When another app sends a request to that URL, your n8n workflow runs immediately, and the request data is handed to every node downstream.
The alternative is polling. A Schedule Trigger or an app-specific polling trigger wakes up on a timer (every minute, every hour) and asks "anything new?" That works, but it has two costs: latency (you only find out at the next poll) and wasted requests (most polls find nothing). Webhooks invert this: the source app pushes data to you the moment the event happens.
| Webhook | Polling / Schedule trigger | |
|---|---|---|
| When it runs | The instant an event happens | On a fixed timer |
| Latency | Real time | Up to one polling interval |
| Direction | Source app pushes to n8n | n8n pulls from source |
| Setup | Source app must support webhooks | Works with any readable API |
| Cost | One request per real event | Many empty requests |
Real-world analogy
A schedule trigger is like checking your mailbox every hour. A webhook is like a doorbell: you are notified the moment something arrives, and nothing is wasted checking an empty mailbox.
Use a webhook when the source app can send one (forms, Stripe, GitHub, most SaaS tools). Fall back to polling when it can only be read on demand.
How the n8n Webhook node works
Drop a Webhook node onto a blank canvas and it becomes the workflow's trigger. n8n generates a unique URL for it of roughly this shape:
https://your-instance.app.n8n.cloud/webhook/abc123-uuid/your-path
The node has a handful of settings that matter:
- HTTP Method — which verb the URL accepts (
GET,POST,PUT, etc.). - Path — the trailing segment of the URL. Defaults to a UUID; you can set a
readable path like
lead-intake. - Authentication — None, Header Auth, Basic Auth, or JWT.
- Respond — how and when n8n replies to the caller.
When a matching request hits the URL, n8n executes the workflow and exposes the
request to expressions. Each item carries the parsed body, headers, and
query so you can read exactly what was sent.
Test URL vs production URL: the #1 gotcha
Every Webhook node has two URLs, and confusing them is the most common reason a webhook "stops working."
| Test URL | Production URL | |
|---|---|---|
| Path prefix | /webhook-test/... | /webhook/... |
| When it listens | Only after you click "Listen for test event" | Whenever the workflow is active |
| How many requests | Captures one, then stops | Every request, indefinitely |
| Purpose | Building and debugging in the editor | Live, deployed automations |
The test URL exists so you can capture a real payload while building: click Listen for test event, fire one request, and n8n freezes that data so you can map fields into downstream nodes. It deliberately stops after one request.
The production URL only works once you flip the Active toggle in the top-right of the editor. Until then, hitting it returns a 404.
Most common mistake
If your webhook worked yesterday and is dead today: confirm the workflow is
active, and confirm the sending app is configured with the production
URL (/webhook/...), not the test URL (/webhook-test/...). Nine out of ten
"it broke" reports are one of these two.
HTTP methods, paths, and authentication
Methods
Match the method to whatever the sender uses. Form tools and event providers
almost always send POST with a JSON body. A health check or a link someone
clicks in a browser is usually a GET. If the sender uses POST but your node
is set to GET, the request simply will not match and n8n returns a 404.
Paths
A custom path makes URLs readable and lets you run several webhooks on one
instance, for example crm-intake, stripe-events, and github-push. Paths can
also include parameters like :id if you need to capture a segment of the URL.
Authentication
The node offers built-in auth so you do not have to validate credentials by hand:
| Option | How the caller authenticates | Good for |
|---|---|---|
| None | No check (anyone with the URL) | Internal or already-signed payloads |
| Header Auth | A named header must equal a stored secret | Most API integrations |
| Basic Auth | Username and password (HTTP Basic) | Legacy tools, simple internal use |
| JWT Auth | A signed JSON Web Token in the header | Apps that already issue JWTs |
| Query token | A ?token=... value you check manually | Quick gates, browser-triggered URLs |
These secrets live in n8n credentials, which keeps them out of the workflow itself. See n8n credentials explained for how that storage works.
Header Auth is the sane default
For most third-party integrations, Header Auth is the cleanest choice: you give the sending app a header name and a secret value, n8n rejects anything missing or wrong, and the secret never appears in the URL or logs.
Reading the incoming data
Once a request arrives, n8n parses it into an item you reference with expressions. The three parts you care about:
- Body —
{{ $json.body }}(the JSON or form fields the sender posted) - Headers —
{{ $json.headers }}(content type, signatures, user agent) - Query —
{{ $json.query }}(anything after the?in the URL)
So an email field posted in the body is read as {{ $json.body.email }}, and a
token in the query string is {{ $json.query.token }}. A signature header sent
by Stripe is {{ $json.headers['stripe-signature'] }}.
Here is a typical payload a form tool might POST. Pull values out of body:
{
"headers": {
"content-type": "application/json",
"user-agent": "Tally/1.0"
},
"params": {},
"query": {},
"body": {
"event": "form.submitted",
"formId": "wMVx7e",
"respondentId": "3X4ab",
"fields": {
"name": "Asha Rao",
"email": "asha@example.com",
"company": "Northwind"
}
}
}
To grab the email from that, a downstream node uses
{{ $json.body.fields.email }}.
Responding to the caller
By default the Webhook node returns a generic 200 OK once the workflow starts,
which is fine for fire-and-forget events. But forms, APIs, and signed providers
often need a specific reply. The Respond dropdown on the Webhook node
controls this:
- Immediately — replies the moment the workflow starts (no custom body).
- When Last Node Finishes — waits, then returns the last node's data.
- Using Respond to Webhook Node — you control the reply explicitly.
For the last option, add a Respond to Webhook node anywhere downstream. It lets you set the status code, headers, and body:
{
"received": true,
"leadId": "lead_8842",
"message": "Thanks, we got your submission."
}
You can return a 200 for success, a 400 when validation fails, or a 401
when auth is rejected. This matters because many providers retry on non-2xx
responses, so returning the right code is part of correct behavior.
Webhooks have a timeout
The caller is holding a connection open while it waits for your reply. If the workflow takes too long (long AI calls, slow third-party APIs), the request can time out on the sender's side. For slow work, respond immediately with a 200, then continue processing in the background.
Real-world examples
Form intake (Tally / Typeform). Point the form's webhook integration at your
production URL. Map {{ $json.body.fields.email }} into a new
Airtable row, then post a heads-up to
Slack or Telegram. This is the backbone of
most lead generation flows.
Payment events (Stripe). Stripe POSTs events like checkout.session.completed.
Set the Webhook to accept POST, verify the Stripe-Signature header against
your signing secret, then provision access, send a receipt, or update your
records. Return a 200 quickly so Stripe does not retry.
Repository events (GitHub). GitHub sends a POST for pushes, pull requests,
and issues, signed with an X-Hub-Signature-256 header. Verify it, branch on the
{{ $json.body.action }} value, and notify the right channel or kick off a
deploy.
Chat triggers. A Telegram or Discord bot, or a custom chat front-end, can hit a webhook on each message. This is a common entry point for AI agent workflows, where the message text feeds a model and the reply goes back through a Respond to Webhook node.
You can test any of these from your terminal before wiring up the real sender:
curl -X POST \
https://your-instance.app.n8n.cloud/webhook/lead-intake \
-H "Content-Type: application/json" \
-H "x-webhook-secret: your-secret-here" \
-d '{"event":"form.submitted","fields":{"name":"Asha Rao","email":"asha@example.com"}}'
Security hardening
A public URL that triggers automation deserves real protection:
- Verify a secret. Use Header Auth, or for signed providers, recompute the signature from the raw body and your secret and compare it to the header. Reject mismatches before doing anything else.
- Validate the payload shape. Do not assume fields exist. Add an
IFnode that checks required fields are present and well-formed, and respond400when they are not. - Always use HTTPS. n8n Cloud handles this for you; self-hosted setups must terminate TLS at a reverse proxy.
- Mind replay and rate concerns. An attacker who captures a request can resend it. Where the provider gives you an event ID or timestamp, dedupe on it and reject stale events. Put a proxy or firewall in front of public endpoints if you expect abuse.
- Keep secrets in credentials, not the URL. A
?token=in the query string can leak into logs and referrers; a header secret is safer.
Validate, then act
Treat every incoming request as untrusted until it passes auth and payload validation. Verify the secret, confirm the fields, then run your logic. This one habit prevents most webhook abuse.
When a webhook isn't firing: debugging checklist
Work through these in order; the failure is almost always near the top.
- Is the workflow active? The production URL is dead unless the Active toggle is on. For test runs, did you click "Listen for test event" first?
- Test URL vs production URL. Confirm the sender uses
/webhook/..., not/webhook-test/.... - HTTP method match. If the sender uses
POSTand the node expectsGET(or vice versa), the request will not match and returns a 404. - Path match. Check the path segment, including trailing slashes and case.
- Authentication. A wrong or missing header/token returns a
401or403. Read the status code the sender actually receives. - Payload format. Sending form-encoded data when you expect JSON (or vice
versa) changes where the data lands. Inspect
bodyin an execution. - Reachability (self-hosted). Can the public internet reach your instance?
Confirm
WEBHOOK_URLis set to your external HTTPS domain and the port is exposed through your proxy or tunnel. Test withcurlfrom outside your network, not from the same machine. - Check executions. Open the workflow's Executions tab. If you see runs, the webhook fired and the bug is downstream. If you see nothing, the request never arrived, so the problem is steps 1 to 7.
Webhook not firing?
Book a debugging call and we'll trace the request live and get it working.
Limitations to keep in mind
- Self-hosted reachability. A localhost instance cannot receive third-party webhooks. You need a public HTTPS endpoint via a reverse proxy or tunnel.
- Timeouts. Long-running workflows can exceed the caller's timeout. Respond fast and process asynchronously when work is slow.
- One request per test listen. The test URL captures a single request by design. For repeated testing, re-arm it or switch to the production URL.
- Payload size and retries. Very large bodies may be rejected, and many providers retry on non-2xx responses, so returning the correct status code is part of behaving well.
Build real-time automations on n8n
Spin up an n8n instance and start wiring webhooks to forms, payments, and chat in minutes.
Affiliate link — we may earn a commission at no extra cost to you.
Keep learning
Webhooks are most powerful when combined with the rest of the toolkit. Pair them with credentials for safe secret storage and expressions for reading payloads. New here? The beginner's guide and what is n8n lay the foundation, and you can explore the n8n tool page for more on the platform itself.