Fixing CORS Errors on an n8n Webhook: Calling a Workflow From the Browser
By Clément Lacaille (Tech-Bharat) · Published 22 September 2026 · 6 min read · how we build and test
Complete FlowKit Bundle — €269
12 ready-to-import n8n workflows, setup guide in French, 14-day guarantee
It's a familiar scenario: you build a Webhook node in n8n, test it with curl or Postman, and everything responds perfectly. Then you wire up a simple fetch() from your site's JavaScript, and the browser console shows a red line with no call made: "Access to fetch at '...' from origin '...' has been blocked by CORS policy." Nothing changed on the n8n side between the two tests — it's the browser enforcing a check that curl simply ignores. This guide explains what CORS actually does, why the obvious Webhook node setting isn't always enough, and the pattern that works in every case, including on n8n Cloud.
What a CORS error actually means
CORS (Cross-Origin Resource Sharing) isn't an n8n feature: it's a security rule enforced by every modern browser. By default, a page loaded from https://mysite.com isn't allowed to read the response of a request sent to a different domain (your n8n instance, https://n8n.mycompany.com) — unless that second domain explicitly says it allows it, via the Access-Control-Allow-Origin header.
Two cases apply:
- Simple request (GET with no special header, or POST with
Content-Type: text/plain): the browser sends the request directly, then checks the response header before handing it to your code. - "Preflighted" request (this is the case for almost every call to an n8n webhook: POST with
Content-Type: application/json, or a custom authentication header): the browser first sends an OPTIONS request to ask for permission, and only sends the actual request if the response to that preflight contains the right headers.
It's this second case that trips up most n8n integrations: the webhook itself is flawless, but nothing correctly answers the OPTIONS request sent right before it.
The Webhook node's Allowed Origins (CORS) option, and its limit
The Webhook node exposes, under the Options tab, an Allowed Origins (CORS) setting: a comma-separated list of domains, or * by default to allow everyone. That's the obvious first move, and it's necessary — but it only tells part of the story. This setting (like the custom headers you add via a Respond to Webhook node) applies to the response of your actual request. Plenty of n8n community reports document cases where the OPTIONS preflight request doesn't get those same headers, because no node in your workflow is set up to answer it: your webhook only listens on POST, and n8n has nothing configured to handle an OPTIONS request on that same path.
The symptom is unmistakable: the CORS error names your correctly configured origin, the call works fine in curl, but the browser still blocks it — and the Network tab in dev tools shows an OPTIONS request that fails or returns a 404, never followed by the POST request.
Instance-level fix (self-hosted): the N8N_CORS_* variables
If you run your own n8n instance (Docker, a VM, PM2...), the cleanest fix is set once, outside the workflow editor entirely. Two environment variables:
| Variable | Role | Recommended production value |
|---|---|---|
N8N_CORS_ENABLED |
Turns on n8n's built-in CORS middleware, which correctly answers OPTIONS requests before they even reach your nodes | true |
N8N_CORS_ALLOW_ORIGIN |
Comma-separated list of allowed origins | Your exact domain, never * |
Add these two lines to your .env file or your container configuration, restart the instance, and the OPTIONS preflight is handled at the level of n8n's own HTTP server — no need for a dedicated node on every webhook. It's the n8n equivalent of the environment variables you already configure for credentials or payload size limits.
Universal fix (n8n Cloud or a shared instance): a dedicated OPTIONS node
On n8n Cloud, you don't control the instance's environment variables. The workaround that works reliably, whatever your plan, is to duplicate your entry point:
- Add a second Webhook node, on the same path as your main webhook, but with the HTTP method set to
OPTIONS. - Wire it directly to a Respond to Webhook node that returns a
204status (no content) with three headers:Access-Control-Allow-Origin(your domain),Access-Control-Allow-Methods(POST, OPTIONS), andAccess-Control-Allow-Headers(Content-Type, plus any custom header your call sends, such as an API token). - Leave your main (
POST) webhook unchanged, with its ownAllowed Origins (CORS)set consistently.
The browser sends the OPTIONS request, hits this dedicated node, gets an explicit answer within milliseconds, then sends the actual POST request, which runs normally through your business logic.
A more robust alternative: don't expose the webhook to the browser
CORS isn't a problem to work around at any cost — it's a signal. If your webhook is meant to stay reserved for your own application, the real question is whether calling it directly from the browser is the right architecture at all. A key empirical study on the topic, by Chen, Jiang, Duan, Wan, Chen, Paxson and Yang, "We Still Don't Have Secure Cross-Domain Requests: an Empirical Study of CORS" (USENIX Security Symposium, 2018), measured that a significant share of sites that configure CORS do so too permissively — reflecting the request's origin without validation, or pairing it with authentication cookies — opening the door to forged requests from a malicious third-party site. The practical takeaway: never leave Allowed Origins at * on a webhook that writes data or triggers an action, and avoid pairing it with sensitive credentials.
Where possible, the safest approach is still to route the call through your own backend (a serverless function, an API route in your framework) that then relays the request to n8n server-side — without ever exposing the webhook's URL directly to the browser. The CORS problem disappears entirely, since the browser-to-your-server call stays same-origin, and the server-to-n8n call is never subject to that policy in the first place. It's also the perfect opportunity to add an authentication and signature verification layer that the browser, by its very nature, can never keep secret.
The special case of Chat Trigger
If what you need is a chat widget rather than a form or a business API, don't reinvent this mechanism by hand: the Chat Trigger node and the official @n8n/chat widget natively handle their own Allowed Origins setting, with more predictable preflight behavior than the generic Webhook node. Our guide to integrating an AI chat widget on a website covers that specific configuration — the patterns in this article mainly apply to a "classic" webhook called from your own code (a form, an app, an API).
That's exactly the use case for the RAG question-answering API workflow in the RAG Assistant Pack (€119): an n8n endpoint designed to be called from a third-party application, with authentication and the response format already set up — all that's left is wiring the CORS headers to your own domain.
Key takeaways
A CORS error on an n8n webhook that otherwise works perfectly almost always points to the same blind spot: the OPTIONS preflight request, not the actual request. On a self-hosted instance, N8N_CORS_ENABLED and N8N_CORS_ALLOW_ORIGIN fix the problem once and for all at the server level. On n8n Cloud, a second Webhook node dedicated to the OPTIONS method reproduces the same behavior by hand. And if the webhook exposes a sensitive action, the best protection against a bad CORS configuration is still to never expose it directly to the browser — a server-side relay solves both the technical problem and the security risk that comes with it.
FAQ
Frequently asked questions
Why does my n8n webhook work in Postman or curl but not from my website?
Because the CORS (Cross-Origin Resource Sharing) policy is enforced by the browser, not the server. curl, Postman, or a server-to-server call never run this check: they send the request and read the response, period. A browser first checks that the server explicitly allows the origin (domain) that loaded the page before letting your JavaScript read the response — and for a non-trivial request (JSON, custom headers), it even sends a preliminary OPTIONS request to ask permission first.
Is the Webhook node's "Allowed Origins (CORS)" setting enough to fix the problem?
It covers the response to your actual request (GET, POST...), but plenty of n8n community reports show it isn't always enough to satisfy the OPTIONS preflight request the browser sends before that, especially with a Content-Type of application/json or custom headers. If the CORS error persists even though the origin is set correctly, it's almost always that preflight failing, not the actual request.
How do I enable CORS at the instance level on self-hosted n8n?
Two environment variables set when the container or process starts: N8N_CORS_ENABLED=true to turn on n8n's built-in CORS middleware, and N8N_CORS_ALLOW_ORIGIN=https://yourdomain.com to restrict accepted origins (the default, *, accepts any site). These variables act before the request even reaches your nodes, which fixes both the actual request and the OPTIONS preflight.
What about n8n Cloud, where I can't set these environment variables?
The reliable workaround is to add a second Webhook node on the same path, configured for the OPTIONS method, which responds immediately via a Respond to Webhook node with the Access-Control-Allow-Origin, Access-Control-Allow-Methods and Access-Control-Allow-Headers headers and a 204 status code. The browser then gets an explicit answer to its preflight before sending the actual request to the regular POST node.