A NetSuite Suitelet CORS configuration determines whether a browser-based application hosted on one origin can call a Suitelet hosted on another. To make cross-origin requests work, the Suitelet must return the correct HTTP response headers, including `Access-Control-Allow-Origin`, and must answer browser preflight `OPTIONS` requests when the request uses methods or headers that trigger preflight. The safest configuration allows only known origins, limits permitted methods and headers, and avoids exposing credentials unless the integration genuinely requires them.
Cross-Origin Resource Sharing, or CORS, is enforced by the browser, not by NetSuite itself. A server-to-server integration does not need browser CORS headers, while a JavaScript application running on another domain does. This distinction is the starting point for troubleshooting Suitelet access. Adding `Access-Control-Allow-Origin: *` does not solve every error, and it is incompatible with credentialed browser requests.
This article focuses on configuring and troubleshooting CORS for Suitelets, rather than explaining what a Suitelet is generally. For the broader overview of Suitelet capabilities, use our guide to the general Suitelet setup and use cases.
What does CORS configuration for Suitelets actually control?
CORS controls whether a browser permits frontend JavaScript to read a response from a different origin. An origin consists of the scheme, host, and port. For example, these are different origins:
`https://app.example.com`
`https://admin.example.com`
`http://app.example.com`
`https://app.example.com:8443`
If a web application at `https://app.example.com` calls a Suitelet at a NetSuite domain, the browser compares the Suitelet response with the request’s `Origin` header. The response must contain a compatible `Access-Control-Allow-Origin` value before the browser exposes the response to the calling script.
CORS does not authenticate the request, authorize the NetSuite user, encrypt data, or make a public Suitelet safe. It is a browser permission mechanism. Authentication still depends on the method used by the Suitelet, such as an authenticated NetSuite session, token-based authentication, or another supported integration pattern.
The main CORS response headers have separate jobs:
| Header | Purpose |
|---|---|
| `Access-Control-Allow-Origin` | Identifies which origin may read the response |
| `Access-Control-Allow-Methods` | Lists methods permitted for cross-origin requests |
| `Access-Control-Allow-Headers` | Lists non-simple request headers the browser may send |
| `Access-Control-Allow-Credentials` | Allows credentials such as cookies when set to `true` |
| `Access-Control-Expose-Headers` | Lets browser JavaScript read selected response headers |
| `Vary: Origin` | Helps caches distinguish responses generated for different origins |
The `Origin` request header is supplied by the browser. A Suitelet should not blindly reflect any value it receives into `Access-Control-Allow-Origin`. A safer pattern is to compare the incoming origin with an allowlist and return the header only when the origin is approved.
Why does a Suitelet request trigger a CORS preflight?
A browser sends a preflight request when the actual cross-origin request is not considered “simple.” The preflight uses the `OPTIONS` method and asks whether the target permits the intended method and headers.
For example, a frontend request like this typically triggers preflight:
fetch(suiteletUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer example-token'
},
body: JSON.stringify({ action: 'refresh' })
});The browser may first send a request resembling:
OPTIONS /app/site/hosting/scriptlet.nl?... HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: authorization, content-typeThe Suitelet must return a successful response that permits the requested method and headers. If the Suitelet only adds CORS headers to the `POST` response but does not handle `OPTIONS`, the browser blocks the request before the `POST` reaches the Suitelet.
This is one of the most important details in Suitelet CORS troubleshooting. Developers frequently test the business logic directly, see that the `POST` works in an API client, and assume the browser should work as well. API clients do not enforce browser CORS rules, and they generally do not reproduce the preflight sequence.
How should we implement CORS in a Suitelet?
A practical implementation has three parts: define approved origins, respond to preflight requests, and apply CORS headers consistently to the actual response.
The following SuiteScript 2.1 example demonstrates the pattern:
/**
* @NApiVersion 2.1
* @NScriptType Suitelet
*/
define([], () => {
const allowedOrigins = new Set([
'https://app.example.com',
'https://admin.example.com'
]);
function setCorsHeaders(response, origin) {
if (!allowedOrigins.has(origin)) {
return false;
}
response.setHeader({
name: 'Access-Control-Allow-Origin',
value: origin
});
response.setHeader({
name: 'Vary',
value: 'Origin'
});
response.setHeader({
name: 'Access-Control-Allow-Methods',
value: 'GET, POST, OPTIONS'
});
response.setHeader({
name: 'Access-Control-Allow-Headers',
value: 'Content-Type, Authorization'
});
return true;
}
function onRequest(context) {
const origin = context.request.headers.origin || '';
const originAllowed = setCorsHeaders(context.response, origin);
if (context.request.method === 'OPTIONS') {
if (!originAllowed) {
context.response.statusCode = 403;
context.response.write('Origin not allowed');
return;
}
context.response.statusCode = 204;
return;
}
if (!originAllowed) {
context.response.statusCode = 403;
context.response.write('Origin not allowed');
return;
}
if (context.request.method === 'GET') {
context.response.setHeader({
name: 'Content-Type',
value: 'application/json'
});
context.response.write(JSON.stringify({
ok: true
}));
return;
}
if (context.request.method === 'POST') {
context.response.setHeader({
name: 'Content-Type',
value: 'application/json'
});
context.response.write(JSON.stringify({
ok: true
}));
return;
}
context.response.statusCode = 405;
context.response.write('Method not allowed');
}
return { onRequest };
});This example is a starting pattern, not a complete authentication design. The allowlist must contain the real frontend origins, including the correct scheme and port. A trailing slash is not part of an origin, so `https://app.example.com/` should not be used as the comparison value.
The response also uses `Vary: Origin`. That header matters when an intermediary cache could store a response generated for one origin and reuse it for another. If the Suitelet returns different `Access-Control-Allow-Origin` values depending on the request, caches need to know that the origin affects the response.
The exact response behavior should be verified in the target NetSuite account and release. Header support, deployment behavior, authentication, and domain routing all belong in testing because a technically correct CORS policy cannot compensate for an inaccessible deployment or an authentication method the browser cannot use.
What CORS headers should a Suitelet return?
A Suitelet should return only the headers required by the frontend contract. Broad headers create unnecessary exposure and make future troubleshooting harder.
For a read-only `GET` endpoint, the response may need only:
Access-Control-Allow-Origin: https://app.example.com
Vary: OriginFor a JSON `POST`, the browser may require:
Access-Control-Allow-Origin: https://app.example.com
Vary: Origin
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-TypeIf the frontend sends `Authorization`, that exact header must appear in `Access-Control-Allow-Headers`. If it sends a custom header such as `X-Request-ID`, that header must also be included. Header names are generally case-insensitive, but matching the browser’s requested names makes diagnostics clearer.
Do not automatically add every possible method or header. `PUT`, `PATCH`, and `DELETE` should not be permitted if the Suitelet does not implement them. Similarly, allowing `Authorization`, custom tracing headers, and other values expands the contract and should reflect a genuine requirement.
`Access-Control-Expose-Headers` is separate from `Access-Control-Allow-Headers`. The former controls which response headers browser JavaScript can read. For example:
Access-Control-Expose-Headers: X-Request-IDWithout that header, the browser may receive `X-Request-ID` while preventing frontend code from reading it.
Should Suitelet CORS allow credentials?
Credentialed CORS requires a more restrictive policy. If the browser must send cookies or another browser-managed credential, the response must include:
Access-Control-Allow-Credentials: trueIt must also return one explicit origin. This combination is invalid:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: trueThe wildcard origin cannot be used with credentialed requests. A Suitelet should therefore identify the requesting origin, verify it against a trusted allowlist, and return that exact origin only when approved.
Credentials deserve special attention in NetSuite because browser session behavior depends on the domain, cookie policy, authentication flow, and request context. A CORS header does not force the browser to send cookies. The frontend request must also opt into credentials, for example:
fetch(suiteletUrl, {
method: 'GET',
credentials: 'include'
});That approach should be used only when the session-based design is intentional. For many integrations, a browser should call a controlled backend rather than directly exposing a privileged NetSuite endpoint to frontend JavaScript. The backend can handle authentication, validation, rate limiting, and response shaping without placing sensitive credentials in browser code.
Why does CORS work in Postman but fail in the browser?
The most common reason is that Postman and similar API clients do not enforce the browser’s same-origin policy. A successful Postman request proves that the endpoint responded to that client. It does not prove that a browser is allowed to expose the response to JavaScript.
A browser failure can occur at several different stages:
The preflight fails. The `OPTIONS` response has an error status, redirects, or omits a required CORS header.
The origin does not match. The Suitelet allows `https://app.example.com`, but the frontend is running at `https://www.app.example.com`, `http://localhost:3000`, or another origin.
The requested header is not allowed. The browser asks for `authorization`, but the response permits only `content-type`.
The request uses credentials incorrectly. The frontend includes credentials while the response uses a wildcard origin or omits `Access-Control-Allow-Credentials`.
The endpoint redirects. A redirect to a login page, a different NetSuite domain, or another deployment can disrupt the expected CORS exchange. The final response needs to be examined, not just the initial URL.
Use the browser’s developer tools to inspect the `OPTIONS` request and the actual request separately. Check the request’s `Origin`, `Access-Control-Request-Method`, and `Access-Control-Request-Headers`, then compare them with the response headers. This is more reliable than copying only the browser’s short console error.
Is wildcard CORS safe for a public Suitelet?
Wildcard CORS is appropriate only for genuinely public, non-sensitive data where any website may read the response. It is not an access-control mechanism and should not be used to make a private Suitelet “work.”
A wildcard policy allows browser JavaScript from any origin to read a response, subject to the endpoint’s other controls. If the Suitelet returns customer information, financial data, inventory details, internal identifiers, or operational actions, a wildcard policy is too broad.
A secure design separates three decisions:
Who can reach the endpoint? This is controlled through deployment and authentication.
Who can perform the operation? This is controlled through authorization and server-side validation.
Which browser origins can read the response? This is controlled through CORS.
These controls overlap, but none replaces the others. A trusted origin can still be compromised, and a correctly configured CORS policy cannot stop a valid authenticated user from invoking an operation they are authorized to perform.
For browser-facing Suitelets, validate inputs on the server, avoid trusting hidden form values, restrict methods, return generic error messages where appropriate, and log meaningful diagnostic information without exposing secrets. CORS headers should be part of the endpoint’s security review, not an afterthought added after a console error appears.
When is a backend proxy better than direct Suitelet access?
A backend proxy is the better design when the browser should not communicate directly with NetSuite or when the integration needs centralized security controls. The browser calls the application backend using the application’s normal authentication. The backend then calls NetSuite through a server-side integration method.
This architecture avoids placing NetSuite credentials in browser code and gives the backend a controlled location for:
Authentication and token management
Authorization checks
Input validation and schema enforcement
Rate limiting
Retries and timeout handling
Audit logging
Data transformation and field filtering
Direct browser-to-Suitelet access remains reasonable for a narrowly scoped interface when the authentication model, exposure, and CORS policy are deliberately designed. It becomes less attractive when multiple frontend applications need access, when sensitive actions are exposed, or when the frontend requires broad NetSuite permissions.
For larger integration designs, our NetSuite integration platform services cover API selection, authentication, middleware, custom SuiteScript, and operational controls. The central decision is not simply whether CORS can be enabled. It is whether the browser should be a direct integration participant at all.
A practical Suitelet CORS testing checklist
Test CORS from the same environment and origin that will run in production. A local development origin such as `http://localhost:3000` is different from a deployed HTTPS application, so both need deliberate treatment.
Use browser developer tools to confirm that:
The `Origin` value is present and matches an approved origin exactly.
The preflight returns a successful status when one is required.
`Access-Control-Allow-Methods` includes the intended method.
`Access-Control-Allow-Headers` includes every requested non-simple header.
Credentialed requests use an explicit origin and `Access-Control-Allow-Credentials: true`.
The actual response includes the necessary CORS headers, not only the preflight response.
The Suitelet does not redirect to an unexpected login or domain.
The response content type matches the frontend’s parsing logic.
Error responses also include appropriate CORS headers when the frontend needs to read them.
A command-line test can help reproduce a preflight without relying on the browser:
curl -i -X OPTIONS "https://example.suitelet.url" \
-H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: content-type, authorization"The URL above is illustrative. Use the deployed Suitelet URL from the relevant NetSuite account and deployment. A successful test should be followed by a test of the real request, because preflight success does not prove that the `POST` or `GET` response is correct.
If the design involves several systems, contact Versich to review the endpoint architecture, authentication model, and CORS policy before exposing a production Suitelet.
Conclusion
A reliable NetSuite Suitelet CORS configuration depends on more than adding one response header. The Suitelet must recognize approved origins, answer `OPTIONS` preflight requests, permit the exact methods and headers required by the frontend, and handle credentials with an explicit origin. Security controls must remain separate from CORS, because browser permissions do not replace authentication or authorization.
Start by determining whether the caller is a browser. If it is not, remove CORS from the problem and focus on the integration’s authentication and transport design. If it is a browser, inspect the preflight exchange, use an origin allowlist, test the actual response, and consider a backend proxy when direct NetSuite access would expose unnecessary risk.
