If you’re trying to call the Squarespace API from browser JavaScript, you’ll run into CORS fast.
That usually looks like this:
fetch("https://api.squarespace.com/1.0/sites", {
headers: {
Authorization: "Bearer YOUR_TOKEN"
}
})
And then the browser smacks you with a CORS error.
The annoying part is that your token might be valid, the endpoint might be correct, and the API might work perfectly in cURL or Postman. But the browser still blocks it. That’s not a Squarespace bug. That’s the browser enforcing Cross-Origin Resource Sharing.
Here’s the practical version: if you’re building a Squarespace integration, you should assume your frontend cannot directly call privileged Squarespace API endpoints unless Squarespace explicitly allows your origin and headers. Most of the time, the right fix is a backend proxy.
What CORS is actually doing
CORS is the browser’s permission system for cross-origin HTTP requests.
If your frontend runs on:
https://myapp.com
and it tries to call:
https://api.squarespace.com
that’s cross-origin. The browser checks the API response for CORS headers before exposing the response to JavaScript.
The key response header is:
Access-Control-Allow-Origin
If the API returns:
Access-Control-Allow-Origin: https://myapp.com
or:
Access-Control-Allow-Origin: *
then the browser may allow the response, depending on the rest of the request.
A real-world example helps. 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
That means browser code can call many GitHub API endpoints cross-origin, and GitHub also explicitly exposes useful non-simple headers like ETag and rate-limit metadata.
If Squarespace doesn’t return equivalent CORS headers for the endpoint you need, the browser blocks access. End of story.
Why Squarespace requests often trigger preflight
Simple cross-origin GETs can sometimes work without a preflight. But the moment you add Authorization, JSON Content-Type, or non-simple methods like PUT and DELETE, the browser usually sends an OPTIONS preflight first.
Example browser request:
fetch("https://api.squarespace.com/1.0/sites", {
method: "GET",
headers: {
Authorization: "Bearer YOUR_TOKEN",
Accept: "application/json"
}
})
Because of the Authorization header, the browser may send:
OPTIONS /1.0/sites HTTP/1.1
Origin: https://myapp.com
Access-Control-Request-Method: GET
Access-Control-Request-Headers: authorization
For that to succeed, Squarespace would need to answer with something like:
Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Methods: GET
Access-Control-Allow-Headers: Authorization
If any part is missing, the browser never sends the actual API request in a usable way for your frontend.
The mistake I see all the time
People test the API in Postman, see a 200 response, and assume the frontend should work too.
Postman is not a browser. cURL is not a browser. Your Node.js script is not a browser. They do not enforce CORS.
This works fine:
curl https://api.squarespace.com/1.0/sites \
-H "Authorization: Bearer YOUR_TOKEN"
That tells you the API and token are valid. It tells you nothing about browser compatibility.
If you want to inspect CORS behavior properly, use browser DevTools or a header inspection tool like HeaderTest. I use tools like that to quickly verify whether an API returns Access-Control-Allow-Origin, whether preflight responses are sane, and which headers are exposed to frontend code.
What you can do from the browser
If Squarespace exposes a public endpoint with permissive CORS, you can call it directly:
async function getPublicData() {
const res = await fetch("https://api.squarespace.com/some-public-endpoint");
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
return res.json();
}
But for authenticated API access, especially with bearer tokens, you should expect this to fail in-browser for CORS reasons or become a bad security idea even if it works.
Why bad security? Because putting a Squarespace API token in frontend JavaScript means you’ve effectively published it to every visitor.
That’s a hard no.
The correct pattern: backend proxy
The browser talks to your server. Your server talks to Squarespace.
That gives you three big wins:
- No browser CORS dependency on Squarespace
- No API token exposed to users
- Better control over caching, validation, and rate limiting
Here’s a minimal Node/Express proxy:
import express from "express";
const app = express();
app.get("/api/squarespace/sites", async (req, res) => {
try {
const apiRes = await fetch("https://api.squarespace.com/1.0/sites", {
headers: {
Authorization: `Bearer ${process.env.SQUARESPACE_TOKEN}`,
Accept: "application/json"
}
});
const text = await apiRes.text();
res.status(apiRes.status);
res.set("Content-Type", apiRes.headers.get("content-type") || "application/json");
res.send(text);
} catch (err) {
console.error(err);
res.status(502).json({ error: "Bad gateway" });
}
});
app.listen(3000, () => {
console.log("Server running on http://localhost:3000");
});
Now your frontend calls your own origin:
async function loadSites() {
const res = await fetch("/api/squarespace/sites");
if (!res.ok) {
throw new Error(`Request failed: ${res.status}`);
}
return res.json();
}
No cross-origin request to Squarespace from the browser. No frontend secret leakage.
Adding CORS to your own proxy
If your frontend and backend are on different origins, then your backend needs to allow your frontend.
Example Express setup:
import express from "express";
import cors from "cors";
const app = express();
app.use(cors({
origin: "https://www.myfrontend.com",
methods: ["GET", "POST", "PUT", "DELETE"],
allowedHeaders: ["Content-Type", "Authorization"],
exposedHeaders: ["ETag"]
}));
If you need cookies or HTTP auth between your frontend and proxy, don’t use *. You need an explicit origin:
app.use(cors({
origin: "https://www.myfrontend.com",
credentials: true
}));
And your frontend must opt in too:
fetch("https://api.mybackend.com/api/squarespace/sites", {
credentials: "include"
});
A serverless version
If you don’t want to run an Express app, a serverless function works well.
Example in a Next.js route handler:
export async function GET() {
try {
const apiRes = await fetch("https://api.squarespace.com/1.0/sites", {
headers: {
Authorization: `Bearer ${process.env.SQUARESPACE_TOKEN}`,
Accept: "application/json"
},
cache: "no-store"
});
const body = await apiRes.text();
return new Response(body, {
status: apiRes.status,
headers: {
"Content-Type": apiRes.headers.get("content-type") || "application/json"
}
});
} catch (err) {
return Response.json({ error: "Bad gateway" }, { status: 502 });
}
}
Frontend:
const res = await fetch("/api/squarespace/sites");
const data = await res.json();
That’s usually enough.
Debugging the failure properly
When a Squarespace browser request fails, check these in DevTools:
1. Was there a preflight?
Look for an OPTIONS request before the real request.
2. Did the response include CORS headers?
You want to see things like:
Access-Control-Allow-Origin
Access-Control-Allow-Methods
Access-Control-Allow-Headers
3. Are you trying to read non-exposed headers?
Even if the request succeeds, JavaScript can only read a limited header set unless the server includes:
Access-Control-Expose-Headers
GitHub does this well. That’s why frontend apps can access headers like ETag and X-RateLimit-Remaining. If Squarespace doesn’t expose a header, response.headers.get("...") returns null even though the browser received it.
Example:
const res = await fetch("/api/example");
console.log(res.headers.get("ETag"));
If your own proxy wants frontend code to read ETag, expose it in your CORS config.
Don’t “fix” CORS in the browser
You’ll find terrible advice online:
- use
mode: "no-cors" - install a browser extension
- disable browser web security
mode: "no-cors" gives you an opaque response you can’t actually use:
const res = await fetch("https://api.squarespace.com/1.0/sites", {
mode: "no-cors"
});
console.log(res.type); // "opaque"
You won’t get JSON, status, or readable headers. It’s useless for API integration.
Browser extensions and disabled security flags are just local hacks. They do nothing for real users.
Security side note
CORS is not an authentication mechanism. It doesn’t protect your API token. It only controls whether browsers let one origin read another origin’s responses.
If you’re building the proxy endpoint, still handle the usual security basics:
- authenticate your users if needed
- validate input
- rate limit expensive endpoints
- avoid reflecting arbitrary upstream URLs
- set proper security headers on your own app
If you’re tightening the rest of your header policy too, csp-guide.com is useful for Content Security Policy work. Different problem than CORS, but they tend to come up together.
The practical rule for Squarespace API work
If the request needs a Squarespace token, don’t call it directly from frontend code.
Use a backend or serverless proxy. Keep the token server-side. Treat direct browser access as a special case that only works when Squarespace clearly supports it with the right CORS headers.
That rule saves a lot of wasted debugging time, and honestly, it’s the architecture you probably wanted anyway.