How to Fix n8n 'Webhook Not Registered' 404 Errors Behind a Reverse Proxy

Introduction

You put n8n behind a reverse proxy, the editor loads fine, and then an external service calls your webhook and gets HTTP 404 with the body The requested webhook "..." is not registered. The workflow looks correct, the URL looks correct, and toggling things at random does not help. This tutorial walks through the three root causes behind that error, in the order you should check them: test-versus-production URL confusion, a missing N8N_WEBHOOK_URL, and reverse-proxy misconfiguration. It also covers the two traps that catch people chaining workflows together: sub-workflow webhooks and route-parameter paths. It is aimed at anyone self-hosting n8n in Docker behind Nginx, Caddy, Nginx Proxy Manager, Traefik, or Cloudflare Tunnel. Every error string below was verified against n8n’s own source code (lab install, version 2.41.4) and cross-checked with an end-to-end reproduction on 2.43.4.

Quick answer

The 404 means n8n has no live route for that method and path. Fix it by clicking Listen for test event (test URLs), activating the workflow (production URLs), setting N8N_WEBHOOK_URL behind a reverse proxy, and copying the full Production URL.

Evidence

n8n is one of the most widely self-hosted automation tools in existence. A live API check on 2026-10-18 showed the n8n-io/n8n GitHub repository at 207,794 stars, and the n8nio/n8n Docker image at 272,919,830 pulls from Docker Hub. With that many self-hosted installs sitting behind reverse proxies, webhook URL problems are among the most-searched n8n errors, which is why n8n’s own docs call the fix out explicitly:

“n8n creates the webhook URL by combining N8N_PROTOCOL, N8N_HOST and N8N_PORT. If n8n runs behind a reverse proxy, that won’t work. That’s because n8n runs internally on port 5678 but the reverse proxy exposes it to the web on port 443.”

The docs continue with the prescribed fix, quoted verbatim from n8n’s reverse-proxy webhook guide: “set the webhook URL manually with the N8N_WEBHOOK_URL environment variable so that n8n can display it in the editor UI and register the correct webhook URLs with external services. (N8N_WEBHOOK_URL replaces WEBHOOK_URL, which is deprecated from n8n 2.35.0; n8n logs a deprecation warning if you still use WEBHOOK_URL.)”

The 404 itself is equally informative. n8n raises it from webhook-not-found.error.js, and the message plus its hint tell you exactly which mistake you made. The three variants are reproduced in Step 1 below. For the general behavior of both URL types, see the official Webhook node documentation.

Prerequisites

  • A self-hosted n8n instance, preferably via Docker Compose. The N8N_WEBHOOK_URL variable needs n8n 2.30.0 or later; on older versions use WEBHOOK_URL. If you are still setting n8n up, follow our Docker Compose setup guide for n8n first.
  • A domain name pointing at a reverse proxy (Nginx, Caddy, Nginx Proxy Manager, Traefik, or Cloudflare Tunnel) that forwards to n8n’s port 5678.
  • SSH access to the server and curl for testing.
  • Admin access to the n8n editor so you can change environment variables and toggle workflows active.

Step-by-step guide

Step 1: Read the 404 body, it names your mistake

Call the failing webhook with curl -i and read the JSON body, not just the status code:

curl -i -X POST https://n8n.example.com/webhook/orders

n8n returns one of three messages, and each one points at a different fix:

Variant A, you called the test URL without listening:

{"code":404,"message":"The requested webhook \"orders\" is not registered.","hint":"Click the 'Execute workflow' button on the canvas, then try again. (In test mode, the webhook only works for one call after you click this button)"}

You hit /webhook-test/orders while the editor was not listening. Go to Step 2.

Variant B, you called the production URL but the workflow is inactive:

{"code":404,"message":"The requested webhook \"POST orders\" is not registered.","hint":"The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"}

Note the POST prefix in the message: n8n includes the HTTP method for production-URL misses. Go to Step 2.

Variant C, the path is registered but your HTTP method is wrong:

{"code":404,"message":"This webhook is not registered for GET requests. Did you mean to make a POST request?"}

The webhook exists, but the node expects POST and you sent GET (browsers always send GET, which is why pasting a webhook URL into the address bar fails). Match the method and retry.

All three strings come straight from n8n’s webhook-not-found.error.js, verified in the lab on version 2.41.4, so you can match them character for character.

Step 2: Use the right URL in the right state

Every Webhook trigger node shows two URLs, and they behave completely differently:

Test URLProduction URL
Path prefix/webhook-test/<path>/webhook/<path>
RequiresEditor listening (click Listen for test event or Execute workflow/step)Workflow toggled Active (top-right of the editor)
Calls allowedExactly one per click, then it 404s againUnlimited
Result visibleLive on the canvasOnly in the Executions tab

The single most common cause of this 404 is mixing them up: testing with the Production URL while the workflow is still inactive, or calling the Test URL twice after a single click. Decide which one you need, put n8n in the matching state, and retry. With the default Respond: Immediately setting, a successful production call returns {"message":"Workflow was started"} and creates a row in Executions. If the workflow is active and the method matches but the 404 persists, the problem is the reverse proxy or the URL itself, which is what Steps 3 and 4 fix.

Step 3: Tell n8n its public URL

If the Webhook node panel shows URLs like http://localhost:5678/webhook/orders or https://n8n.example.com:5678/webhook/orders, no external service can ever reach them. n8n builds those URLs from what it knows about itself, and behind a reverse proxy it only knows its internal address. The official docs quoted above prescribe the fix: set the webhook base URL explicitly.

For Docker Compose, add two variables to the n8n service:

services:
  n8n:
    image: docker.n8n.io/n8nio/n8n:2.43.4
    environment:
      - N8N_WEBHOOK_URL=https://n8n.example.com/
      - N8N_PROXY_HOPS=1

Then recreate the container:

docker compose up -d

A plain docker compose restart does not pick up new environment variables; you must recreate the container. After it comes back, reopen the workflow and check the node panel: both URLs should now start with https://n8n.example.com/, with no port number.

Version rules, verified against n8n’s docs and startup logs:

  • n8n 2.30.0 and later: use N8N_WEBHOOK_URL.
  • Older versions: use WEBHOOK_URL.
  • From 2.35.0, WEBHOOK_URL is deprecated. It still works, but n8n prints this at startup: WEBHOOK_URL -> Use N8N_WEBHOOK_URL instead, which sets the base URL for both test and production webhooks.
  • Never set both at once.

Beware the half-fix: setting only N8N_HOST and N8N_PROTOCOL produces https://n8n.example.com:5678/webhook/... on current versions, because n8n still does not know the proxy listens on 443. The internal port leaks into the URL. Only N8N_WEBHOOK_URL gives you a clean public URL. If you are changing versions at the same time, read our guide to upgrading and restarting n8n safely before touching the compose file.

Step 4: Make the reverse proxy pass what n8n needs

Even with the right URL, the 404 persists if the proxy mangles the request on the way in. Two things must be true: n8n must trust the proxy’s forwarding headers, and the proxy must forward the path unchanged.

First, n8n’s web server (Express) ignores X-Forwarded-* headers unless you tell it how many proxies sit in front of it. Without N8N_PROXY_HOPS, every login through a proxy logs:

ValidationError: The 'X-Forwarded-For' header is set but the Express 'trust proxy' setting is false (default). ... ERR_ERL_UNEXPECTED_X_FORWARDED_FOR

Set N8N_PROXY_HOPS=1 for one reverse proxy (added in the compose snippet in Step 3). If Cloudflare’s proxy sits in front of your own proxy, that is two hops, so use 2.

Second, the proxy must forward the request path to n8n exactly as it arrived. This Nginx server block has the shape that works:

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 80;
    server_name n8n.example.com;

    location / {
        proxy_pass http://127.0.0.1:5678;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Host $host;
    }
}

Three details matter for webhooks specifically:

  1. proxy_pass http://127.0.0.1:5678; has no trailing slash inside location /. A trailing slash tells Nginx to rewrite the URI, so n8n receives a different path than the one in the registered webhook and answers “not registered”.
  2. The Host header must be the public domain, not the upstream name. If n8n sees Host: n8n:5678, its origin checks and URL logic break in confusing ways.
  3. If you serve n8n under a subpath (for example /n8n/), your rewrite rules must still deliver /webhook/<path> to n8n byte-for-byte. Any prefix stripping that alters the webhook path produces the same 404.

Apply with nginx -t && systemctl reload nginx (or nginx -s reload). For Cloudflare Tunnel, point the tunnel’s public hostname at http://localhost:5678 with no path rewriting; the tunnel itself is transparent to webhook paths.

Step 5: Sub-workflow webhooks, active, public, and unique

A frequent pattern is chaining workflows: a parent workflow calls a child workflow through an HTTP Request node pointed at the child’s webhook URL. Three rules keep this working behind a reverse proxy:

The child workflow must be independently Active. Calling a workflow with the Execute Workflow node is a different mechanism: n8n runs the child directly and skips its triggers entirely. But the moment you address the child over HTTP, its webhook behaves like any other production webhook, and an inactive child answers 404 with the Variant B message from Step 1.

Copy the child’s Production URL after Step 3, not before. A URL copied while N8N_WEBHOOK_URL was unset still points at localhost:5678 and fails from anywhere except the n8n server itself. Reopen the child’s Webhook node and copy the fresh Production URL into the parent’s HTTP Request node.

Every webhook path must be unique per HTTP method across all active workflows. Duplicating a workflow to build a “sub-workflow” variant keeps the same webhook path, and n8n detects the collision at activation time: activating the copy fails with HTTP 409 and the message There is a conflict with one of the webhooks. (verified in n8n’s workflow.service.js). Rename the copy’s path (for example orders-child) before activating it.

Watch out for route parameters. If the path contains a :name segment, such as orders/:orderId, n8n prepends the node’s internal webhookId (a UUID) to the registered route. The node’s own help text says it plainly: “If dynamic values are set ‘webhookId’ would be prepended to path.” The registration code confirms it (webhook.path.includes(':') ? webhook.webhookId : undefined). So the real URL looks like /webhook/9f3a1c2e-…/orders/:orderId, and you call /webhook/9f3a1c2e-…/orders/123. Never hand-type these URLs; always copy the Production URL from the node panel.

Step 6: Verify end to end

With the workflow active and the proxy fixed, call the production URL from outside your network:

curl -X POST https://n8n.example.com/webhook/orders \
  -H 'Content-Type: application/json' \
  -d '{"orderId":"123"}'

Expect {"message":"Workflow was started"} and a new execution in the Executions tab. Then trigger it through the real external service (Stripe, GitHub, or whatever calls it in production).

If it still 404s, run the diagnostic ladder:

  1. On the server, curl -X POST http://127.0.0.1:5678/webhook/orders. A 200 here means the proxy is still the problem; a 404 here means the problem is inside n8n (wrong URL type, inactive workflow, or path mismatch).
  2. Re-read the exact 404 body and match it to Variant A, B, or C in Step 1.
  3. Reopen the Webhook node and compare the Production URL character by character with what the caller uses, especially after any route-parameter UUID prefix.

Troubleshooting

“This webhook is not registered for GET requests. Did you mean to make a POST request?” The path is fine but the method is wrong. Browsers send GET, so pasting a POST webhook URL into the address bar always fails this way. Test with curl -X POST, Postman, or an HTML form, and make sure the external service is configured for the method the node expects.

The webhook works with curl on the server but 404s through the domain. The proxy is rewriting the path or forwarding to the wrong upstream. Check for a trailing slash in proxy_pass, subpath location blocks with rewrites, or Cloudflare Tunnel public-hostname rules that alter the path. Compare curl -i output from both routes: same path, same method, different result means the proxy changed something.

The node panel still shows localhost:5678 after setting N8N_WEBHOOK_URL. You restarted the container instead of recreating it, so the new variable never loaded. Run docker compose up -d, wait for n8n to boot, then reopen the workflow. Also check you did not set both N8N_WEBHOOK_URL and the deprecated WEBHOOK_URL at once.

Activating a workflow fails with “There is a conflict with one of the webhooks.” Another active workflow already owns that method and path combination, usually a duplicated draft you forgot about. Deactivate the duplicate or give the new workflow a unique path, then activate again.

A route-parameter webhook 404s even though everything looks active. You are calling /webhook/orders/123, but n8n registered /webhook/<node-uuid>/orders/:orderId because the path contains :orderId. Open the node, copy the Production URL exactly, and use that. This also applies to sub-workflow URLs stored in a parent’s HTTP Request node: re-copy them after any node is recreated, because a new node means a new UUID.

FAQ

The six most common questions are answered in the FAQ metadata at the top of this article and summarized here: test URLs work once per click by design; use N8N_WEBHOOK_URL on n8n 2.30.0+ and the legacy WEBHOOK_URL only on older versions; production calls appear in Executions, never on the canvas; you can expose just /webhook/ paths publicly while keeping the editor private; Cloudflare Tunnel needs no path rewriting and counts toward N8N_PROXY_HOPS; and route parameters trigger a UUID prefix in the registered URL that you should copy rather than type.

Conclusion

The “not registered” 404 is n8n telling you precisely what is wrong, once you know its three dialects: test URL without listening, production URL without an active workflow, or the wrong HTTP method. Behind a reverse proxy, the durable fix is N8N_WEBHOOK_URL plus N8N_PROXY_HOPS, a proxy that forwards the path and headers unchanged, and unique, correctly copied webhook URLs for every workflow in the chain. Next, if your webhooks are about to take real traffic, read our Docker Compose setup guide for n8n to harden the deployment they run on, or our walkthrough of installing n8n on a VPS if you are still choosing where to host.

Frequently asked questions

Why does my n8n test webhook work once and then return 404?

By design, a test webhook URL only works for one call after each click of Listen for test event (or Execute workflow). The 404 hint says it directly: in test mode, the webhook only works for one call after you click the button. Click Listen again before every test call, or switch to the Production URL with an active workflow.

Should I use WEBHOOK_URL or N8N_WEBHOOK_URL?

Use N8N_WEBHOOK_URL on n8n 2.30.0 or later. On older versions use WEBHOOK_URL. From n8n 2.35.0, WEBHOOK_URL is deprecated and n8n logs a deprecation warning at startup if you still set it. Never set both at the same time.

Do production webhook calls show data on the workflow canvas?

No. Unlike test calls, production webhook calls never appear on the canvas. The error hint states this explicitly. Open the workflow's Executions tab to see production runs.

Can I keep the n8n editor private and expose only webhooks publicly?

Yes. Configure your reverse proxy to forward only the /webhook/ and /webhook-test/ paths to n8n, and set N8N_WEBHOOK_URL to the public domain so the node panel shows reachable URLs. This is the standard pattern for letting external services like Stripe or GitHub reach n8n while the editor stays internal.

Anything special when using Cloudflare Tunnel in front of n8n?

Point the tunnel's public hostname at n8n's local port with no path rewriting, so /webhook/... arrives unchanged. If Cloudflare's proxy also sits in front of your own reverse proxy, that counts as two hops, so set N8N_PROXY_HOPS=2 instead of 1.

Why does my webhook URL contain a long UUID before my path?

Because your path contains a route parameter (a :name segment). n8n prepends the node's internal webhookId to the registered route in that case, so orders/:orderId becomes /webhook/<uuid>/orders/:orderId. Always copy the Production URL from the node panel instead of typing it by hand.

TP
ToolPilot Team

We install, break, and fix automation tools so you don't have to. Every tutorial on ToolPilot is tested hands-on before publishing.

Keep reading