NodeRecipe.

n8n Webhook Tutorial: Receive, Authenticate and Respond (2026)

Build an n8n webhook properly — the two URLs, authentication, response modes, CORS, binary uploads, and the reverse-proxy setting that breaks production URLs.

By Ali Ilyas · · Updated August 24, 2026

A webhook is how everything else on the internet starts an n8n workflow. Your form, your app, Stripe, a CRM — they all do the same thing: send an HTTP request to a URL you own, and expect a sensible response back.

The Webhook node makes the receiving half trivial. The parts that take a day are the ones nobody demonstrates: deciding when to respond, letting a browser call it without tripping CORS, accepting a file upload, and making the production URL correct when n8n sits behind a reverse proxy.

This is the build guide. If your webhook exists and simply isn't firing, that is a different problem with a different checklist — workflows that don't trigger covers activation, URL modes and path conflicts in diagnostic order.

What's on this page

The shape of an incoming request

Before configuring anything, know what you get. Every request arrives as one item with four top-level keys:

  • headers — everything the caller sent, including content type, signatures and your auth header.
  • params — path parameters, if your path defines any.
  • query — the query string, parsed.
  • body — the request body, parsed according to its content type.

So body.email and headers['x-signature'] are the expressions you will write most. The commonest beginner error is reaching for email directly and getting undefined, because the payload is nested one level deeper than expected.

Build against reality rather than assumption: put the node in listening mode, fire one real request from the actual sender, and read what arrives. Ten seconds of that beats any amount of reading the sender's documentation.

Setting up the node

Add a Webhook node — it is a trigger, so it sits at the start — and set three things:

HTTP Method. POST for anything sending data, GET for anything a browser opens or a service pings to check you are alive. The method is part of the webhook's identity, not a filter: a GET and a POST on the same path are two different webhooks that coexist happily.

Path. n8n generates a UUID. Keep it, or set something readable like stripe-invoice-paid. Two considerations pull in opposite directions — readable paths are easier to operate, and a guessable path on an unauthenticated webhook is an open endpoint. Readable path plus authentication is the right combination.

Respond. The one that decides your architecture. Covered below.

Everything else lives under Options, and the useful ones are worth knowing before you need them: Allowed Origins for CORS, Binary Property for file uploads, Raw Body when you need the unparsed payload, Response Headers, Ignore Bots, and an IP allowlist that returns 403 to everyone else.

The two URLs, briefly

The node shows a Test URL and a Production URL, and they are not interchangeable. The test URL only listens while you have clicked Execute workflow in the editor. The production URL only works while the workflow is active.

The one thing to internalise while building: test the thing you are about to ship on the production URL before you call it done. A webhook that works in the editor and 404s once activated is the most common false finish in n8n, and it has caught enough people that the not-triggering guide treats it as the first diagnostic step. It also has a specific consequence for third-party verification, covered below.

Choosing how to respond

Four modes, and the difference between them is when the caller gets an answer:

ModeCaller receivesUse when
ImmediatelyA response code and "Workflow got started"The sender only needs an acknowledgement — most third-party services
When Last Node FinishesThe output of the last node executedYou want the workflow's result and don't need to shape it
Using 'Respond to Webhook' NodeWhatever you define, wherever you place itAPIs, form endpoints, anything with error paths
Streaming responseOutput streamed as it is producedChat-style responses; requires nodes that support streaming, such as the AI Agent

Immediately is right more often than people assume. A service delivering an event does not want your workflow's output — it wants a 200 so it can stop retrying. Responding immediately also means a slow workflow cannot cause the sender to time out and re-deliver, which is a real cause of duplicate processing.

When Last Node Finishes has a sub-setting worth knowing: Response Data chooses between All Entries, First Entry JSON, First Entry Binary, and No Response Body. First Entry JSON is what most people want and is not always the default.

Streaming response is the newest of the four and exists for chat interfaces, where waiting for a complete answer feels broken. It only does something if the workflow contains a node that can stream — an AI agent, typically.

The Respond to Webhook node

Once you need control over status codes, headers, or different answers on different branches, you need this node. The Webhook node's Respond option must be set to Using 'Respond to Webhook' Node, then place it wherever the response should be sent from.

What it gives you:

  • Status codes that mean something. 400 for a bad payload, 401 for a failed check, 200 for success. A form front end can then behave correctly instead of guessing.
  • Response body you shape. JSON, plain text, HTML, or no content.
  • Custom headers, including the CORS headers discussed below.
  • Redirects, which is how you send a form submitter to a thank-you page.

Two behaviours to design around:

One response per execution. The first Respond to Webhook node reached ends the response. A workflow with several branches needs one on each branch, and only the branch that runs will answer.

A failing workflow does not automatically send an error response. If the workflow throws before reaching a Respond node, the caller does not get the 500 you might expect it to. When the caller's behaviour depends on receiving an error, build that path explicitly: an error branch that ends in a Respond to Webhook node with the right status code.

Large binary responses deserve care. Returning a big file through this node holds the response open while the data moves. Where the file already lives somewhere addressable, returning a redirect or a signed URL is faster and much less fragile than streaming bytes back through the workflow.

Authentication

The node supports four settings, and the default is the wrong one for anything real:

MethodHow the caller proves itselfGood for
NoneIt doesn'tLocal experiments only
Basic authUsername and passwordInternal callers you control
Header authA named header with a secret valueMost third-party integrations
JWT authA signed tokenCallers that already issue JWTs

Header auth is the practical default. Almost every service that calls webhooks can add a custom header, and n8n stores the expected value as a credential rather than in the workflow.

Two limits to be clear about:

This is not signature verification. Stripe, Slack, GitHub and others sign their payloads with a shared secret, and verifying that signature is the only way to know a request genuinely came from them. n8n's built-in auth checks a header you configured, which a third party sending you events cannot set. For those callers, use no built-in auth and verify the signature yourself in a Code node — the same reasoning as the Slack signature point.

An unauthenticated webhook is a public endpoint. Anyone with the URL can run your workflow as often as they like. If the workflow costs money — an AI call, an API with metered pricing — that is a bill anyone can run up. Combine authentication with the IP allowlist option where the caller has stable addresses.

Calling a webhook from a browser: CORS

If a page's JavaScript calls your webhook, the browser sends a preflight OPTIONS request first and refuses the real call unless the response allows the origin. This is the most common "my webhook works in Postman but not from my site" problem, and it is entirely a browser rule — nothing is wrong with n8n.

The supported fix is the Allowed Origins (CORS) option on the Webhook node. Set it to your site's origin, or to several, and n8n handles the preflight for you.

Three practical notes:

  • Set the origin, not a wildcard, once you are past testing. A wildcard means any site can call your endpoint from any visitor's browser.
  • Multiple domains go in the same option, comma-separated. Reaching for a reverse-proxy rule to add CORS headers is a common detour and usually unnecessary.
  • Custom headers need to be allowed too. If your front end sends an auth header, the preflight asks about that header specifically, and a configuration that allows the origin but not the header still fails.
  • The method has to be allowed as well as the origin. A preflight failing with "Method PUT is not allowed by Access-Control-Allow-Methods" is not an origin problem, and re-checking your Allowed Origins will not fix it — the browser is asking about the verb. A webhook node listens for one HTTP method; if the front end sends PUT or PATCH, either match it or enable Allow Multiple HTTP Methods. Read the browser's error text closely, because "origin", "header" and "method" are three different failures that all present as "CORS".

If you are terminating TLS at Caddy, Traefik or nginx, make sure only one layer sets CORS headers. Two layers both adding Access-Control-Allow-Origin produces a duplicate-header error that reads as though neither is configured.

Receiving files

To accept a file upload, enable the Binary Property option. The uploaded file arrives as binary data on the item rather than as text in the body, and downstream nodes read it by that property name.

The two things that go wrong:

Name mismatches. Whatever the binary property is called on the webhook must match what the next node looks for. Most n8n nodes default to data. A mismatch produces an empty-input error that reads like a corrupt upload.

Size limits. There is a maximum payload size, and it is a setting on your instance rather than a property of the node. Large uploads fail with a payload-too-large error that has nothing to do with your workflow. On self-hosted instances that limit is an environment variable — see the Docker setup guide — and if a reverse proxy sits in front, it has its own separate limit that will bite first.

For anything above a few megabytes, prefer having the caller upload to storage and send you a URL. Webhooks are a poor file transport, and every layer between the internet and n8n gets an opinion about the size.

Behind a reverse proxy

Self-hosted n8n behind Caddy, nginx or Traefik has one specific failure that wastes an afternoon: the URL n8n displays is not the URL the outside world should call. It shows something with an internal port — http://localhost:5678/webhook/... — because that is what the process knows about itself.

The fix is to tell n8n its public address with the WEBHOOK_URL environment variable. Set it to your public HTTPS base URL, restart, and the displayed URLs become the ones you can actually hand to a third party.

"I set WEBHOOK_URL and it's still showing localhost." Three things account for nearly every case, in order:

  1. The container was never recreated. Editing the variable in a compose file and running docker compose restart does not apply it. docker compose up -d does. This is the answer most of the time.
  2. The variable is set in the wrong place — on the host shell rather than in the container's environment, or in a .env the compose file does not actually read.
  3. The proxy is rewriting the port. If n8n now reports your domain but with :5678 appended, or requests reach n8n on a port your proxy config disagrees with, that is an nginx/Caddy/Traefik upstream problem rather than an n8n one. It produces the specific misery of a webhook that works in the editor and dies silently in production, because the editor never leaves the machine.

Check the variable inside the running container before changing anything else — the value n8n actually sees is the only one that matters.

This matters more than cosmetics. Services that verify a webhook by calling it back, or that reject non-HTTPS endpoints, fail against the internal URL — and the error they return usually says nothing about ports. Any service demanding an HTTPS callback needs this set correctly, which is part of putting n8n on a real domain.

Verification handshakes

Many platforms verify ownership before they will deliver events: they send a GET with a challenge parameter and expect it echoed back, often within seconds.

Three things make this fail on n8n specifically:

  1. The wrong URL. Verification must be done against the production URL with the workflow active. Passing verification on the test URL and then activating leaves the service pointing at a URL that stops answering — and a service that verified once may not tell you it has started failing.
  2. The wrong method. The challenge usually arrives as a GET while your events arrive as POST. Those are two separate webhooks in n8n. You need a node for each, or one node handling GET for verification and another handling POST for events.
  3. The wrong response. Most challenges want the raw value echoed as plain text, not wrapped in JSON. Use a Respond to Webhook node returning text.

Get all three right and the handshake succeeds first time. Get one wrong and the platform reports a generic verification failure that names none of them.

Security beyond authentication

Verify signatures where the sender offers them. Any sender who signs payloads has given you something stronger than a shared header, and the check is a few lines in a Code node.

Use the IP allowlist for fixed-address callers. Many services publish their outbound ranges. Requests from anywhere else get a 403 without touching your workflow.

Treat the payload as untrusted. It arrived from the internet. Validate before you write it to a database or paste it into a model prompt.

Never put secrets in the path. A path is not a secret: it appears in logs at every hop. Secrets belong in headers, which is what the auth options use.

Turn on the bot-ignoring option for public paths. Crawlers and link previewers will hit any URL that leaks, and each hit is an execution you paid for.

Gotchas

Path and method together must be unique. Two active workflows sharing both won't publish. The not-triggering guide covers the diagnosis.

The body is nested. It's body.field, not field. Pin a real request while building so the expression editor shows you the true shape.

Duplicate deliveries are normal. Senders retry when they don't get a fast 2xx. Respond immediately where you can, and make the workflow idempotent — check whether you have already processed this event ID before acting on it.

Content type decides parsing. A sender posting JSON with a form content type gives you a body that looks like a string. Raw Body plus your own parse is the fix when you cannot change the sender.

Changing the path breaks the caller. The URL is the contract. Change it and every sender must be updated.

Sub-workflows and waits complicate the response. A workflow that waits on a webhook and calls sub-workflows can return data from an earlier node than you expect. If the response body looks stale, check which node actually ended the execution.

Deactivating the workflow takes the endpoint offline. The production URL stops answering immediately, and callers see a 404, not a maintenance message.

Frequently asked questions

Why does my webhook work in the editor but not once activated? The test URL and the production URL are different endpoints. Test listens only while you have clicked Execute workflow; production works only while the workflow is active. Always confirm on the production URL — full diagnosis here.

Why is my webhook URL showing localhost or port 5678? n8n is reporting the address it knows about itself, which is wrong behind a reverse proxy. Set the WEBHOOK_URL environment variable to your public HTTPS base URL and restart.

How do I call an n8n webhook from my website's JavaScript? Set the Allowed Origins (CORS) option on the Webhook node to your site's origin. Without it the browser blocks the call at the preflight stage, even though the same request works from Postman or curl.

Can I allow more than one domain for CORS? Yes — the Allowed Origins option takes several, comma-separated. Adding headers at a reverse proxy instead usually causes duplicate-header errors.

How do I return a proper error to the caller? Use a Respond to Webhook node on an explicit error branch with the status code you want. A workflow that throws before reaching a Respond node does not automatically return the error you would expect.

How do I redirect after a form submission? Set the Webhook node to respond using the Respond to Webhook node, then configure that node to redirect. This is the standard pattern for posting a form directly to n8n.

Why does my webhook fire more than once per event? Usually the sender retrying because it didn't receive a fast 2xx — respond immediately and make the workflow idempotent. Also check for a duplicate active workflow on a different method, and for a test execution running at the same time.

How do I receive a file upload? Enable the Binary Property option. The file arrives as binary data under that property name, which must match what the next node expects — most default to data.

Why does my file upload fail with a payload-size error? You have exceeded the maximum payload size. That is an instance-level setting, not a node option, and any reverse proxy in front has its own limit that often triggers first.

Can I get the raw, unparsed body? Yes — enable Raw Body. This is what you need for signature verification, which must run against the exact bytes received, and for senders using a content type n8n would otherwise parse wrongly.

How do I verify a Stripe, Slack or GitHub signature? Not with the node's authentication options — those check a header you configured, and a third party cannot set it. Take the raw body, compute the HMAC with your shared secret in a Code node, and compare against the signature header before doing anything else.

A service says my webhook failed verification. Why? Three usual causes: you verified against the test URL, the challenge arrives as a GET while your node only accepts POST, or the challenge value needs echoing back as plain text rather than JSON.

What's the difference between Respond Immediately and When Last Node Finishes? Immediately returns a "Workflow got started" acknowledgement without waiting. When Last Node Finishes waits and returns the final node's output, which means a slow workflow can make the caller time out.

Can I stream a response back, for a chat UI? Yes — the Streaming response mode exists for exactly that, and it requires a node in the workflow that supports streaming, such as an AI agent.

Should I use a webhook to call another n8n workflow? No. Use the Execute Sub-workflow node with the When Executed by Another Workflow trigger — no URLs, no path conflicts, and the data passes directly.

Is my webhook URL private? Treat it as public. Anyone with the URL can trigger the workflow, so add authentication, and never rely on a hard-to-guess path as your only protection.

Can I use a webhook on n8n Cloud? Yes, and the URLs are public HTTPS with no proxy configuration needed — the reverse-proxy section applies only to self-hosted instances.

What does "The requested webhook is not registered" mean? n8n received a request for a webhook it isn't currently listening for. Almost always: the workflow is not active, or the request hit the test URL when no test execution was running. It also appears on app triggers — a Telegram trigger showing a 404 with this message usually means the workflow is inactive or another instance has claimed the bot's single webhook slot.

Why does CORS still fail after I set the allowed origin? Because "CORS" covers three separate checks. The browser's message names which one failed: origin, request header, or HTTP method. A PUT or PATCH from your front end against a node listening only for POST fails on the method no matter how the origins are configured.

I set WEBHOOK_URL but the URL still shows localhost. Recreate the container rather than restarting it — docker compose up -d, not restart. If it still persists, confirm the variable is visible inside the container, and check whether your proxy is appending or rewriting a port.

How do I stop spam hitting a public webhook? Layer it: authentication so anonymous callers are rejected outright, the IP allowlist where the caller has stable addresses, and the bot-ignoring option to drop crawlers and link previewers. Then respond immediately, so junk requests cost you one fast response instead of a full workflow run.

Can I run a webhook on a local n8n instance? For receiving from the internet, not directly — a local instance has no public address. Use n8n's tunnel for development, or put it behind a public HTTPS address properly for anything real. The tunnel is a development convenience and is not something to depend on in production.

Can the Chat Trigger take file uploads? It is a webhook underneath, so it accepts more than text, but it is built around the chat interface rather than as a general file endpoint. If uploads are central to what you're building, a plain Webhook node with Binary Property enabled gives you far more control than working against the chat trigger's assumptions.


Primary references: n8n's Webhook node documentation for methods, response modes and options, the Respond to Webhook node reference, and Webhook credentials for the authentication types.

Next steps: if the webhook exists and isn't firing, work through workflows that don't trigger. For a public HTTPS address and a correct WEBHOOK_URL, see self-hosting n8n and the Docker setup guide. Webhooks are also the front door for RAG ingestion and for AI agents exposed to your own front end, and our templates page has importable starting points.