If you try to call the BigCommerce REST Management API directly from browser JavaScript, you’re going to hit a wall. Not because your fetch() code is wrong, but because CORS is doing exactly what it’s supposed to do.

BigCommerce has multiple API surfaces, and they do not behave the same way from a browser. That distinction matters:

  • Storefront APIs are designed for browser-facing use cases
  • Management APIs are meant for trusted server-side access
  • CORS policy decides whether the browser will even allow your frontend code to read the response

That’s the part people usually miss. The request may leave the browser just fine, but if the response doesn’t include the right CORS headers, your app still fails.

The mental model

CORS is a browser enforcement layer. Your server-to-server requests are unaffected.

When a browser on https://app.example.com calls https://api.bigcommerce.com/..., the browser checks response headers like:

Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Headers: Content-Type, X-Auth-Token
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS

If those headers are missing or don’t match the request, the browser blocks your JavaScript from reading the response.

A permissive API might return:

Access-Control-Allow-Origin: *

GitHub’s API is a good real-world example of a public API that is CORS-aware. A real response from api.github.com includes:

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 tells the browser two things:

  1. Any origin can read the response
  2. JavaScript is allowed to read those non-simple response headers

BigCommerce management endpoints generally are not set up for that kind of browser access, and that’s a good thing.

Why direct browser calls to BigCommerce usually fail

A typical BigCommerce admin API call needs an auth header:

X-Auth-Token: YOUR_TOKEN

That alone usually triggers a preflight request because X-Auth-Token is a non-simple header.

The browser sends:

OPTIONS /stores/{store_hash}/v3/catalog/products HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: GET
Access-Control-Request-Headers: x-auth-token

For the real request to proceed, BigCommerce would need to answer with something like:

Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET
Access-Control-Allow-Headers: X-Auth-Token

If it doesn’t, the browser blocks the call before your GET ever happens in a usable way.

Here’s the frontend code people often write first:

async function loadProducts() {
  const response = await fetch(
    "https://api.bigcommerce.com/stores/STORE_HASH/v3/catalog/products",
    {
      headers: {
        "X-Auth-Token": "BIGCOMMERCE_ACCESS_TOKEN",
        "Accept": "application/json"
      }
    }
  );

  const data = await response.json();
  console.log(data);
}

This is exactly the kind of code you should not ship:

  • it exposes a sensitive token in browser code
  • it likely fails CORS
  • even if it worked, anyone could steal the token from DevTools

That’s not a workaround. That’s a breach with extra steps.

The right pattern: proxy BigCommerce through your backend

For BigCommerce admin and management operations, your frontend should call your server, and your server should call BigCommerce.

Browser flow:

Browser -> Your API -> BigCommerce API

This avoids two separate problems:

  1. No BigCommerce token in the browser
  2. No browser-enforced CORS problem against BigCommerce

Example architecture

  • Frontend calls https://app.example.com/api/products
  • Your backend adds the BigCommerce token
  • Backend calls BigCommerce
  • Backend returns sanitized data to frontend

Node.js example with Express

import express from "express";
import cors from "cors";

const app = express();

app.use(cors({
  origin: "https://storefront.example.com"
}));

app.get("/api/products", async (req, res) => {
  const storeHash = process.env.BC_STORE_HASH;
  const token = process.env.BC_ACCESS_TOKEN;

  try {
    const bcResponse = await fetch(
      `https://api.bigcommerce.com/stores/${storeHash}/v3/catalog/products`,
      {
        headers: {
          "X-Auth-Token": token,
          "Accept": "application/json",
          "Content-Type": "application/json"
        }
      }
    );

    const body = await bcResponse.json();

    res.status(bcResponse.status).json(body);
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: "Failed to fetch BigCommerce products" });
  }
});

app.listen(3000, () => {
  console.log("API listening on port 3000");
});

That’s the baseline. For production, I’d tighten it further:

  • authenticate the user before allowing access
  • return only the fields the frontend actually needs
  • rate limit the endpoint
  • log BigCommerce errors without leaking internals to clients

Handling your own CORS on the proxy

Once your frontend talks to your backend, you still need to configure CORS there.

A safe Express config looks like this:

app.use(cors({
  origin: ["https://storefront.example.com"],
  methods: ["GET", "POST", "PUT", "DELETE"],
  allowedHeaders: ["Content-Type", "Authorization"],
  credentials: true,
  maxAge: 600
}));

A couple of opinions here:

  • Don’t use origin: "*" if you also need credentials
  • Don’t reflect arbitrary origins unless you really mean it
  • Keep allowedHeaders tight instead of lazily allowing everything

If you need cookies or session auth from the browser, you also need:

fetch("https://app.example.com/api/products", {
  credentials: "include"
});

And your server must return:

Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: https://storefront.example.com

Not *. Browsers reject wildcard origins on credentialed requests.

Preflight gotchas with custom headers

Even when BigCommerce is no longer in the browser path, your own API can still trip over preflight.

This frontend request causes preflight because of the Authorization header and JSON content type:

await fetch("https://app.example.com/api/products", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer user-session-token"
  },
  body: JSON.stringify({ categoryId: 24 })
});

Your backend must answer the OPTIONS request correctly.

In Express, the cors middleware usually handles this, but if you have custom middleware ordering problems, you may need:

app.options("*", cors());

I’ve seen teams burn hours on this because auth middleware rejects OPTIONS requests before CORS middleware runs. If preflight is failing, check middleware order first.

What about BigCommerce storefront use cases?

This is where people mix things up. “BigCommerce API” is too broad.

Some storefront-facing features are built to work in the browser, especially when tied to shopper context or same-origin store pages. Some management endpoints are absolutely not.

The practical rule is simple:

  • If the API requires a privileged admin token, assume server-side only
  • If it’s meant for storefront/browser use, check the official BigCommerce docs for the exact auth and origin model

Official docs are the only source I’d trust here: https://developer.bigcommerce.com/docs

Don’t infer browser support just because an endpoint returns JSON.

Exposing response headers from your proxy

Sometimes your frontend needs metadata from headers, not just the JSON body. Common examples:

  • pagination links
  • rate limit values
  • ETags

Browsers only expose a small set of “simple” response headers by default. If you want frontend JavaScript to read custom ones, add Access-Control-Expose-Headers.

Example:

app.use(cors({
  origin: "https://storefront.example.com",
  exposedHeaders: ["ETag", "X-RateLimit-Remaining", "Link"]
}));

Then in the browser:

const response = await fetch("https://app.example.com/api/products");
console.log(response.headers.get("ETag"));
console.log(response.headers.get("X-RateLimit-Remaining"));
console.log(response.headers.get("Link"));

This is exactly why GitHub exposes a long list of headers in CORS. Without Access-Control-Expose-Headers, your JavaScript can’t read most of them even if they’re present on the network response.

Debugging CORS against BigCommerce

When debugging, I separate failures into three buckets:

1. Browser CORS failure

You’ll see errors like:

Access to fetch at 'https://api.bigcommerce.com/...' from origin 'https://app.example.com'
has been blocked by CORS policy

That means the browser refused access. Your JavaScript never gets the response.

2. API auth failure

You get an actual HTTP response like 401 or 403.

That means CORS probably succeeded somewhere in the chain, but your token or permissions are wrong.

3. Preflight failure

The browser console mentions OPTIONS, Access-Control-Allow-Headers, or Access-Control-Allow-Methods.

That usually means custom headers, methods, or credentials triggered preflight and the server didn’t answer correctly.

Useful curl trick

curl does not enforce CORS, but it helps you inspect headers:

curl -i -X OPTIONS \
  "https://api.bigcommerce.com/stores/STORE_HASH/v3/catalog/products" \
  -H "Origin: https://app.example.com" \
  -H "Access-Control-Request-Method: GET" \
  -H "Access-Control-Request-Headers: x-auth-token"

If the response doesn’t include the needed Access-Control-Allow-* headers, the browser won’t allow the request.

Security advice I’d actually enforce

If I were reviewing a BigCommerce integration, these would be non-negotiable:

  • Never put BigCommerce admin tokens in frontend code
  • Never rely on CORS as your auth mechanism
  • Use a backend proxy for management APIs
  • Restrict your own proxy CORS to known origins
  • Validate user authorization before forwarding requests
  • Return only the minimum data needed
  • Add proper security headers on your app; if you’re reviewing broader header hardening, https://csp-guide.com is useful for CSP specifically

CORS is not a permission system. It’s a browser read barrier. Your backend still needs real authentication and authorization.

A solid BigCommerce setup

For most teams, the clean setup looks like this:

  1. Browser talks only to your app backend
  2. Backend stores BigCommerce credentials securely
  3. Backend calls BigCommerce APIs server-to-server
  4. Backend returns filtered data to the frontend
  5. Your backend sets a strict CORS policy for your frontend origin

That approach is boring, and boring is good. It’s easier to secure, easier to debug, and it matches how BigCommerce management APIs are supposed to be used.

If your direct browser call to BigCommerce is failing, I’d treat that as a signal, not a bug. The fix usually isn’t “how do I bypass CORS?” The fix is “move this call to the server where it belongs.”