If you build internal tools with Appsmith, you will hit CORS sooner or later.
Usually it happens like this: your API works fine in Postman or curl, then Appsmith tries to call it from the browser and everything blows up with a vague “blocked by CORS policy” error. That is not Appsmith being weird. That is the browser enforcing cross-origin rules exactly as designed.
This guide is the copy-paste version I wish more teams had. No fluff, just what matters for Appsmith apps.
What CORS means for Appsmith
CORS stands for Cross-Origin Resource Sharing. It is a browser security mechanism that decides whether frontend JavaScript can read responses from another origin.
An origin is:
- scheme:
httporhttps - host:
app.example.com - port:
80,443,3000
So these are different origins:
https://appsmith.company.comhttps://api.company.comhttp://api.company.comhttps://api.company.com:8443
If your Appsmith app runs in the browser and calls an API on another origin, the API must explicitly allow that request with the right response headers.
Postman does not care about CORS. Browsers do. Appsmith in the browser does.
The one header everyone starts with
The most basic CORS response header is:
Access-Control-Allow-Origin: https://your-appsmith-domain.com
Or sometimes:
Access-Control-Allow-Origin: *
A real example from api.github.com:
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 useful things:
- Any origin may read the response (
*) - JavaScript may read those listed non-simple response headers
That second one matters more than people think.
Why Appsmith requests trigger CORS errors
Your Appsmith app will trigger CORS checks when:
- it calls an API on another origin
- it sends JSON with
Content-Type: application/json - it sends custom headers like
Authorization,X-API-Key, orX-Requested-With - it uses methods like
PUT,PATCH, orDELETE - it sends cookies or session credentials
A lot of these requests trigger a preflight request first.
The browser sends:
OPTIONS /users HTTP/1.1
Origin: https://appsmith.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: authorization, content-type
Your server must answer correctly before the real request is even attempted.
The minimum CORS response for preflight
Here is the common working shape:
Access-Control-Allow-Origin: https://appsmith.example.com
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type, X-API-Key
Access-Control-Max-Age: 86400
Vary: Origin
And for credentialed requests, also:
Access-Control-Allow-Credentials: true
If you use Access-Control-Allow-Credentials: true, you cannot use Access-Control-Allow-Origin: *. You must return a specific origin.
This is one of the most common mistakes I see.
Appsmith-specific reality
Appsmith can connect to APIs in a few different deployment patterns, but if the browser is making the request, browser CORS rules apply. That means your backend team cannot say “works on the server” and call it done.
For Appsmith, I usually break API integrations into three buckets:
1. Public API, no credentials
Simplest case.
Server response:
Access-Control-Allow-Origin: *
This works if you do not send cookies or auth tied to browser credentials.
2. Token-based API with Authorization header
Very common in Appsmith.
You need preflight support:
Access-Control-Allow-Origin: https://appsmith.example.com
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type
Vary: Origin
3. Session cookie auth
This is where people get burned.
You need all of this:
Access-Control-Allow-Origin: https://appsmith.example.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, X-CSRF-Token
Vary: Origin
And your cookie settings must also be correct, usually including:
Set-Cookie: session=abc123; Secure; HttpOnly; SameSite=None
Without SameSite=None; Secure, cross-site cookies often will not be sent at all.
Copy-paste server configs
Express.js
import express from "express";
import cors from "cors";
const app = express();
const allowedOrigins = [
"https://appsmith.example.com",
"https://appsmith.internal.example.com",
];
app.use(cors({
origin(origin, callback) {
if (!origin) return callback(null, true); // server-to-server or curl
if (allowedOrigins.includes(origin)) return callback(null, true);
return callback(new Error("CORS blocked for origin: " + origin));
},
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
allowedHeaders: ["Authorization", "Content-Type", "X-API-Key"],
exposedHeaders: ["ETag", "Link", "X-RateLimit-Remaining"],
credentials: true,
maxAge: 86400,
}));
app.options("*", cors());
app.get("/api/users", (req, res) => {
res.json([{ id: 1, name: "Ava" }]);
});
app.listen(3000);
If you do not use cookies, set credentials: false and you can loosen things up.
Nginx
location /api/ {
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin "https://appsmith.example.com" always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type, X-API-Key" always;
add_header Access-Control-Allow-Credentials "true" always;
add_header Access-Control-Max-Age 86400 always;
add_header Vary "Origin" always;
return 204;
}
add_header Access-Control-Allow-Origin "https://appsmith.example.com" always;
add_header Access-Control-Allow-Credentials "true" always;
add_header Access-Control-Expose-Headers "ETag, Link, X-RateLimit-Remaining" always;
add_header Vary "Origin" always;
proxy_pass http://backend;
}
Flask
from flask import Flask, jsonify
from flask_cors import CORS
app = Flask(__name__)
CORS(
app,
resources={r"/api/*": {"origins": ["https://appsmith.example.com"]}},
supports_credentials=True,
allow_headers=["Authorization", "Content-Type", "X-API-Key"],
expose_headers=["ETag", "Link", "X-RateLimit-Remaining"],
methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
max_age=86400
)
@app.route("/api/users")
def users():
return jsonify([{"id": 1, "name": "Ava"}])
app.run()
Exposed headers: the part people forget
Even when the request succeeds, browser JavaScript cannot read every response header by default.
If your Appsmith logic needs rate limit info, pagination, ETags, or redirect metadata, your API must expose those headers.
GitHub does this well:
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
If your Appsmith app reads response headers, add them explicitly:
Access-Control-Expose-Headers: ETag, Link, X-RateLimit-Remaining, X-Request-Id
Common CORS failures in Appsmith
“No ‘Access-Control-Allow-Origin’ header is present”
Your API did not return the header at all, or a proxy stripped it.
“Request header field authorization is not allowed”
Your preflight response is missing:
Access-Control-Allow-Headers: Authorization
“The value of the ‘Access-Control-Allow-Credentials’ header is ’’ which must be ’true’”
You are sending credentials, but the server did not opt in.
“The ‘Access-Control-Allow-Origin’ header contains ‘*’ which is not allowed when credentials flag is true”
Classic misconfiguration. Replace * with the exact Appsmith origin.
OPTIONS returns 404 or 405
Your server handles GET and POST but forgot OPTIONS. Preflight dies before your real request starts.
How I debug this quickly
I check three things:
- The browser devtools Network tab
- The preflight
OPTIONSresponse - The exact response headers
If you want a quick way to inspect header behavior and compare what a service is returning, HeaderTest is handy.
For command-line checks, use curl.
Check the actual response
curl -i https://api.example.com/users \
-H "Origin: https://appsmith.example.com"
Simulate preflight
curl -i -X OPTIONS https://api.example.com/users \
-H "Origin: https://appsmith.example.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: authorization,content-type"
You want to see matching Access-Control-Allow-* headers in the response.
Safe production defaults
My default advice for Appsmith-connected APIs:
- do not use
*unless the API is truly public and stateless - allow only known Appsmith origins
- return
Vary: Originwhen origin is dynamic - support
OPTIONScleanly - expose only the response headers your app actually needs
- avoid cookie auth unless you really need it; bearer tokens are usually less painful across origins
A decent baseline response policy looks like this:
Access-Control-Allow-Origin: https://appsmith.example.com
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Expose-Headers: ETag, Link, X-Request-Id
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 86400
Vary: Origin
CORS is not your auth layer
One last thing: CORS is a browser read-permission mechanism, not access control in the real security sense.
If your API is sensitive, protect it with real authentication and authorization. CORS just tells the browser whether frontend code may read the response. It does not stop direct server calls, curl, or abuse from non-browser clients.
If you are tightening broader HTTP response security beyond CORS, things like CSP, frame protections, and related headers matter too. For that side of the stack, https://csp-guide.com is worth keeping around.
When Appsmith talks to your API, CORS needs to be boring. Exact origins, explicit methods, explicit headers, and a working OPTIONS handler. That is the whole game.