If you’re trying to call the Webflow API directly from browser JavaScript, CORS is the first wall you hit.
And honestly, that wall exists for a good reason.
Webflow’s API is meant for authenticated server-side use in most real applications. Frontend devs still try to wire it straight into a Webflow site, React app, or embedded widget because it feels faster. Sometimes it even works during early testing. Then auth headers, preflight requests, token exposure, or browser restrictions ruin the plan.
So here’s the practical comparison guide: when CORS works for Webflow API, when it doesn’t, and what tradeoffs you’re really making.
The short version
If your app needs a Webflow API token, don’t call Webflow directly from the browser.
Use your own backend, serverless function, or edge function as a proxy.
If you’re only talking about public, unauthenticated resources and Webflow explicitly allows cross-origin requests, then browser-to-API can be fine. But that’s not the common case for Webflow API integrations.
What CORS actually changes
CORS is just the browser enforcing cross-origin access rules.
It decides whether JavaScript running on:
https://your-site.com
can read a response from:
https://api.webflow.com
The browser checks response headers like:
Access-Control-Allow-Origin: https://your-site.com
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Allow-Methods: GET, POST, PATCH, DELETE, OPTIONS
If those headers don’t line up with your request, the browser blocks access.
Your server code isn’t subject to browser CORS rules. That’s why the same request that fails in fetch() often works perfectly in Node, Python, or a serverless function.
A useful baseline: public APIs that are browser-friendly
Some APIs are intentionally permissive.
For example, api.github.com sends:
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
That tells me two things:
- GitHub expects some browser-based access.
- GitHub explicitly exposes useful custom headers to frontend JavaScript.
That’s a very different posture from APIs that are mainly designed for private authenticated backend use.
Webflow integrations usually land in the second bucket.
Option 1: Call Webflow API directly from the browser
This is the approach people try first.
const res = await fetch("https://api.webflow.com/v2/sites", {
headers: {
Authorization: `Bearer ${WEBFLOW_TOKEN}`
}
});
const data = await res.json();
console.log(data);
Pros
- Fastest to prototype
- No backend to deploy
- Simple architecture on paper
- Lower operational overhead at first
Cons
- You expose your Webflow token to users
- Preflight requests can fail
- Browser blocks response if CORS headers don’t match
- Hard to control rate limiting and abuse
- You can’t safely perform privileged API actions
- Debugging is annoying because network errors often hide the real CORS issue
My take
For production, this is usually a bad idea.
The token exposure alone is enough to kill it. Even if Webflow allowed the CORS request, putting a bearer token in browser code is not a serious security model.
If someone can open DevTools, they can get your token.
Option 2: Use your server as a proxy to Webflow API
This is the pattern I recommend most of the time.
Browser calls your backend:
Browser -> your-api.com -> api.webflow.com
Your backend stores the Webflow token securely and adds it server-side.
Example with Express
import express from "express";
import fetch from "node-fetch";
const app = express();
app.use(express.json());
app.get("/api/webflow/sites", async (req, res) => {
try {
const response = await fetch("https://api.webflow.com/v2/sites", {
headers: {
Authorization: `Bearer ${process.env.WEBFLOW_TOKEN}`,
Accept: "application/json"
}
});
const data = await response.json();
res.setHeader("Access-Control-Allow-Origin", "https://www.yourdomain.com");
res.json(data);
} catch (err) {
res.status(500).json({ error: "Proxy request failed" });
}
});
app.listen(3000);
Pros
- Keeps Webflow token off the client
- Full control over CORS policy
- Lets you validate input before forwarding requests
- Easier to add caching, auth, and rate limiting
- Better logging and observability
- Lets you reshape Webflow responses for frontend use
Cons
- You now own backend infrastructure
- More moving parts
- Slightly higher latency
- Need to secure your proxy against abuse
My take
This is the boring solution, which usually means it’s the right one.
You can keep the browser-facing CORS policy narrow:
Access-Control-Allow-Origin: https://www.yourdomain.com
Vary: Origin
That’s much better than spraying * everywhere and hoping nothing sensitive leaks.
Option 3: Serverless or edge function proxy
If you don’t want a full backend, use a function.
A small function on Vercel, Netlify, Cloudflare, or similar platforms gives you most of the benefits of a backend proxy without managing a dedicated server.
Example pattern
export default async function handler(req, res) {
if (req.method === "OPTIONS") {
res.setHeader("Access-Control-Allow-Origin", "https://www.yourdomain.com");
res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
return res.status(204).end();
}
const response = await fetch("https://api.webflow.com/v2/sites", {
headers: {
Authorization: `Bearer ${process.env.WEBFLOW_TOKEN}`
}
});
const data = await response.json();
res.setHeader("Access-Control-Allow-Origin", "https://www.yourdomain.com");
res.status(response.status).json(data);
}
Pros
- No long-running server to manage
- Secure token storage
- Easy to deploy
- Good fit for small Webflow-powered projects
- Can handle preflight cleanly
Cons
- Platform limits and cold starts
- Harder local debugging than a traditional backend
- Vendor-specific deployment quirks
- Still another service in your architecture
My take
For Webflow projects, this is often the sweet spot.
You get a secure boundary without turning a marketing site into a giant infrastructure project.
Direct browser CORS vs proxy: side-by-side
Direct browser calls
Best for:
- Truly public data
- Throwaway demos
- Cases with no secret credentials
Bad for:
- Authenticated Webflow API usage
- CMS writes
- Site management actions
- Anything you care about protecting
Proxy/backend calls
Best for:
- Authenticated API access
- CMS sync jobs
- Admin dashboards
- Production sites
Bad for:
- Teams that refuse to run any backend at all
- Ultra-simple public-only experiments
The preflight problem nobody likes
As soon as you send something like:
Authorization: Bearer ...
the browser typically sends an OPTIONS preflight request first.
It asks the server:
- Are these methods allowed?
- Are these headers allowed?
- Is this origin allowed?
If the API doesn’t answer correctly, your actual request never happens from the browser’s point of view.
That’s why “it works in Postman” means almost nothing for CORS troubleshooting. Postman is not a browser. It does not enforce browser CORS policy.
Common mistakes with Webflow API and CORS
1. Hiding the token in frontend code
People do this:
const token = "super-secret-token";
Or they inject it at build time and assume it’s safe.
It’s not safe. Built assets are still client assets.
2. Using Access-Control-Allow-Origin: * everywhere
This is fine for some public resources, but not for authenticated endpoints.
If you’re allowing credentials or exposing sensitive data, wildcard origin is the wrong tool.
3. Forgetting Vary: Origin
If your proxy dynamically allows certain origins, add:
Vary: Origin
Without it, caches can serve the wrong CORS response across origins.
4. Ignoring exposed headers
Even when a cross-origin request succeeds, frontend JavaScript cannot automatically read every response header.
That’s what Access-Control-Expose-Headers is for.
GitHub’s real-world example is a good model here. They explicitly expose headers like ETag, Link, and rate limit headers so browser code can use them.
If you build your own proxy and want the frontend to read custom headers, expose them:
Access-Control-Expose-Headers: ETag, X-RateLimit-Remaining
Security tradeoffs you should actually care about
CORS is not authentication.
CORS does not stop curl, server scripts, or malicious backend access. It only controls what browsers let frontend JavaScript read across origins.
That means your proxy still needs real security:
- user authentication
- authorization checks
- rate limiting
- request validation
- audit logging
If you’re locking down browser access, also think about the rest of your response headers. If you need a broader headers strategy beyond CORS, that’s where something like official header guidance and broader security header references can help. If you specifically branch into CSP, https://csp-guide.com is the one non-official resource I’d keep around.
Best practice recommendation
For Webflow API, I’d rank the options like this:
1. Serverless or backend proxy
Best default choice.
2. Full backend integration
Best for larger systems, dashboards, or multi-user apps.
3. Direct browser calls
Only for public, non-sensitive cases where no secret is involved and Webflow explicitly supports the CORS flow you need.
If your integration touches CMS writes, site settings, or any authenticated Webflow data, keep it off the client.
That’s the clean rule.
A simple production checklist
- Store Webflow tokens only on the server
- Allow only your real frontend origin
- Handle
OPTIONSrequests explicitly - Set
Vary: Originwhen origin is dynamic - Expose only the response headers your frontend needs
- Add rate limiting to your proxy
- Log failed upstream requests
- Never treat CORS as auth
Official docs worth checking
If you’re building against the Webflow API from a browser, the comparison is pretty straightforward: direct CORS access is convenient but fragile and unsafe for authenticated use, while a proxy adds one extra layer and solves most of the real problems. I’d take the extra layer every time.