If you’ve ever tried calling a Magento REST API from a separate frontend and hit a browser error that looks vague, annoying, and borderline insulting, that was probably CORS.

Magento itself usually isn’t the whole problem. The browser is enforcing the policy, your web server is often the layer that needs fixing, and authentication makes everything more fragile.

Here’s the practical version: if your React, Vue, Next.js, or plain JavaScript app lives on https://storefront.example and your Magento API lives on https://shop.example, the browser treats that as cross-origin. Even if both domains are yours.

What CORS actually means for Magento

CORS stands for Cross-Origin Resource Sharing. It’s just a set of HTTP headers that tell the browser whether frontend JavaScript is allowed to read a response from another origin.

If your Magento API doesn’t return the right headers, the request may still reach the server, and the server may even return 200 OK, but the browser will block your app from seeing it.

That mismatch confuses people all the time.

A browser request from:

fetch("https://shop.example/rest/V1/products?searchCriteria[currentPage]=1", {
  headers: {
    Authorization: "Bearer YOUR_TOKEN"
  }
})

will usually trigger CORS checks because:

  • the origin is different
  • Authorization is a non-simple header
  • the browser may send a preflight OPTIONS request first

A quick Magento REST API example

Magento REST endpoints usually look like this:

GET /rest/V1/products
GET /rest/V1/customers/me
POST /rest/V1/carts/mine/items
POST /rest/V1/integration/customer/token

A frontend app might do this:

async function getCustomer(token) {
  const res = await fetch("https://shop.example/rest/V1/customers/me", {
    method: "GET",
    headers: {
      "Authorization": `Bearer ${token}`,
      "Accept": "application/json"
    },
    credentials: "include"
  });

  if (!res.ok) {
    throw new Error(`HTTP ${res.status}`);
  }

  return res.json();
}

That request can fail for CORS even when the token is valid.

The headers you actually need

For Magento REST API, the usual CORS response headers are:

Access-Control-Allow-Origin: https://storefront.example
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type, Accept
Access-Control-Allow-Credentials: true
Vary: Origin

And for preflight responses:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://storefront.example
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type, Accept
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 86400
Vary: Origin

A few rules matter a lot:

1. Access-Control-Allow-Origin cannot be * with credentials

If your frontend sends cookies or uses credentials: "include", you cannot use:

Access-Control-Allow-Origin: *

You must return the exact origin.

2. Preflight must be handled cleanly

If the browser sends OPTIONS and your server returns 404, 405, or a redirect, the real request never happens.

3. Authorization must be explicitly allowed

Bearer tokens are common with Magento. If Authorization isn’t listed in Access-Control-Allow-Headers, the browser blocks the request.

Real-world reference: not every API needs the same CORS shape

A public API can often get away with very permissive CORS.

For example, api.github.com returns:

access-control-allow-origin: *
access-control-expose-headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset, Warning

That works because GitHub supports lots of public read access and exposes useful response metadata to frontend code. Magento storefront APIs are usually more sensitive, and you’ll often need credentialed requests, so copying * everywhere is the wrong move.

Apache config for Magento CORS

If Magento runs behind Apache, I prefer handling CORS at the web server layer instead of trying to bolt it into PHP.

Here’s a practical Apache config using mod_headers and mod_rewrite:

<IfModule mod_headers.c>
    SetEnvIf Origin "^https://storefront\.example$" ACAO=$0

    Header always set Access-Control-Allow-Origin %{ACAO}e env=ACAO
    Header always set Access-Control-Allow-Credentials "true" env=ACAO
    Header always set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" env=ACAO
    Header always set Access-Control-Allow-Headers "Authorization, Content-Type, Accept" env=ACAO
    Header always set Vary "Origin"
</IfModule>

<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteCond %{REQUEST_METHOD} OPTIONS
    RewriteRule ^(.*)$ $1 [R=204,L]
</IfModule>

That’s the basic shape, but I’d rather make the preflight response explicit:

<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteCond %{REQUEST_METHOD} OPTIONS
    RewriteRule ^ - [R=204,END]
</IfModule>

<IfModule mod_headers.c>
    SetEnvIf Origin "^https://storefront\.example$" ACAO=$0

    Header always set Access-Control-Allow-Origin %{ACAO}e env=ACAO
    Header always set Access-Control-Allow-Credentials "true" env=ACAO
    Header always set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" env=ACAO
    Header always set Access-Control-Allow-Headers "Authorization, Content-Type, Accept" env=ACAO
    Header always set Access-Control-Max-Age "86400" env=ACAO
    Header always set Vary "Origin"
</IfModule>

If you support multiple frontend origins, match them carefully:

SetEnvIf Origin "^https://(storefront|admin-app)\.example$" ACAO=$0

Don’t reflect arbitrary origins. That turns CORS into “whoever asks nicely gets access.”

Nginx config for Magento CORS

Nginx is common in Magento deployments, especially with PHP-FPM. This is the version I’d start with:

map $http_origin $cors_origin {
    default "";
    "~^https://storefront\.example$" $http_origin;
}

server {
    listen 443 ssl;
    server_name shop.example;

    location /rest/ {
        if ($request_method = OPTIONS) {
            add_header Access-Control-Allow-Origin $cors_origin always;
            add_header Access-Control-Allow-Credentials "true" always;
            add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
            add_header Access-Control-Allow-Headers "Authorization, Content-Type, Accept" always;
            add_header Access-Control-Max-Age 86400 always;
            add_header Vary "Origin" always;
            return 204;
        }

        add_header Access-Control-Allow-Origin $cors_origin always;
        add_header Access-Control-Allow-Credentials "true" always;
        add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
        add_header Access-Control-Allow-Headers "Authorization, Content-Type, Accept" always;
        add_header Vary "Origin" always;

        try_files $uri $uri/ /index.php?$args;
    }
}

A small warning: if inside Nginx gets abused a lot. For a simple method check with return 204, it’s usually fine. I still keep it scoped tightly.

Exposing Magento response headers to frontend JavaScript

Sometimes your frontend needs to read custom response headers. Browsers hide most non-simple headers unless you explicitly expose them.

Example:

Access-Control-Expose-Headers: ETag, X-Request-Id

If you don’t expose them, this won’t work:

const etag = res.headers.get("ETag");

GitHub’s API is a good example here. It exposes a long list of useful headers like ETag, Link, and rate-limit metadata. That’s why frontend apps can read them safely.

For Magento, you probably only need a short list, if any.

Common Magento CORS failures

Preflight gets redirected

If OPTIONS /rest/V1/... redirects from HTTP to HTTPS, from bare domain to www, or through some auth middleware, the browser may reject it.

Preflight requests should get a direct success response.

Magento returns CORS headers on GET but not on errors

This one is nasty. Your happy-path works, then a 401, 403, or 500 shows up without CORS headers, and the browser reports a generic CORS failure instead of the real API error.

Set headers with always in Apache/Nginx so error responses also include them.

Wildcard origin with cookies

This breaks immediately:

Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

Browsers reject that combination.

Missing Vary: Origin

If you cache API responses at a CDN or reverse proxy, one origin’s CORS response can get served to another origin unless you vary on Origin.

That leads to weird intermittent bugs that only happen in production. My least favorite category.

How to test Magento CORS properly

Browser DevTools is useful, but I like testing the raw headers too.

A preflight request with curl:

curl -i -X OPTIONS "https://shop.example/rest/V1/customers/me" \
  -H "Origin: https://storefront.example" \
  -H "Access-Control-Request-Method: GET" \
  -H "Access-Control-Request-Headers: Authorization, Content-Type"

A real request:

curl -i "https://shop.example/rest/V1/products" \
  -H "Origin: https://storefront.example" \
  -H "Authorization: Bearer YOUR_TOKEN"

If you want a faster way to inspect header behavior from a browser-facing perspective, HeaderTest is handy for checking what your server actually returns.

Security boundaries people get wrong

CORS is not authentication.

Allowing an origin does not mean the caller is trusted. It only means browser JavaScript from that origin can read the response.

If your Magento API is sensitive, you still need proper auth, token handling, CSRF protections where relevant, and solid cookie settings. If you’re reviewing broader response header hardening beyond CORS, I’d also check a resource like csp-guide.com.

A sane default for Magento REST API

If I were setting up a Magento API for a custom storefront, I’d do this:

  • allow only the exact frontend origin
  • allow Authorization, Content-Type, and Accept
  • support OPTIONS with a clean 204
  • send CORS headers on success and error responses
  • use Vary: Origin
  • avoid * if cookies or authenticated requests are involved
  • expose only the response headers the frontend actually needs

That gets you out of 90% of Magento CORS trouble without turning your API into a public free-for-all.