Why Your API Works in Postman but Fails in Browser
Your API request works perfectly in Postman. The same URL, same token, same JSON body, and same method fail from browser JavaScript. Maybe React says TypeError: Failed to fetch. Maybe Chrome shows a CORS error. Maybe your backend never receives the request. This guide explains what is actually different and gives you a practical debugging order.
Quick answer: Postman and cURL are direct API clients. Browser JavaScript is not. The browser adds security rules before your request can reach or expose data from another origin. That means a request can be valid HTTP and still fail in the browser because of CORS, preflight, credentials, cookies, mixed content, redirects, blocked headers, or missing server response headers.
Postman vs browser: the real difference
The first mistake beginners make is assuming Postman and browser fetch are the same kind of client. They are not. Postman is an API testing tool. cURL is a command-line HTTP client. They can send a request directly to the API and show the response. A browser page running JavaScript has to obey browser security rules because it is executing code from a website origin.
An origin is not just the domain name. It includes the scheme, hostname, and port. These are different origins:
https://example.com
http://example.com
https://api.example.com
https://example.com:3000
http://localhost:3000
http://localhost:5173
So when your React app runs at http://localhost:5173 and calls https://api.example.com, the browser sees that as a cross-origin request. The API server must explicitly allow that browser origin to read the response. If it does not, Postman may still work while the browser fails.
| Thing being tested | Postman or cURL | Browser fetch or Axios |
|---|---|---|
| Raw URL, method, token, and body | Yes, direct test | Yes, but browser rules also apply |
| CORS permission | Usually not enforced like a browser | Strictly enforced by the browser |
| Preflight OPTIONS request | Usually not automatically sent | Often sent before the real request |
| Cookie and credential behavior | Controlled by your tool settings | Controlled by fetch credentials, SameSite, Secure, domain, and CORS headers |
| Mixed HTTPS and HTTP | May still make the request | Can be blocked by the browser |
The useful question is not only, “Does the API work?” The better question is, “Does this API allow this browser origin, with this method, with these headers, with this body type, with these credentials, over this protocol?”
CORS is not the API failing. It is the browser refusing access.
CORS means Cross-Origin Resource Sharing. It is a browser security mechanism that decides whether JavaScript running on one origin can access a response from another origin. The important part is this: CORS is controlled by server response headers, not by wishful thinking in frontend code.
A common beginner error looks like this:
Access to fetch at 'https://api.example.com/users'
from origin 'http://localhost:5173' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
This does not always mean the URL is wrong. It means the browser made, or tried to make, a cross-origin request and did not receive the permission header it needed. The API may still work in Postman because Postman is not running inside your website origin.
The server response must allow the browser origin
For a simple public API response without cookies, the server may respond like this:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://localhost:5173
Content-Type: application/json
For public, non-credentialed resources, some APIs use a wildcard:
Access-Control-Allow-Origin: *
But wildcard CORS is not a safe default for private dashboards, student admin panels, paid APIs, cookie-based sessions, or any API that returns user-specific data. If cookies or credentials are involved, the server usually needs to return a specific origin, not *.
Beginner mental model: CORS is not a button you press in React. Your frontend asks. The browser checks. The API server must answer with permission. If the permission is missing or mismatched, the browser blocks JavaScript from reading the response.
Preflight: the hidden OPTIONS request beginners miss
Many “works in Postman but fails in browser” issues happen before the real request. The browser sends a preflight request first. This is an OPTIONS request that asks the server if the real request is allowed.
For example, your frontend wants to send this:
POST /v1/orders
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json
Before sending the real POST, the browser may send something like this:
OPTIONS /v1/orders HTTP/1.1
Origin: http://localhost:5173
Access-Control-Request-Method: POST
Access-Control-Request-Headers: authorization, content-type
The server must answer the preflight with headers that allow the requested origin, method, and headers:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://localhost:5173
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 600
If the server rejects OPTIONS, redirects it to login, requires authentication on it, returns a 404, returns a 405, or forgets to include the requested headers, the browser may never send your real POST. This is why your backend logs can look empty even though your frontend code “made a request.” The browser stopped at the permission check.
Common preflight failures
| Preflight symptom | Likely meaning | What to check next |
|---|---|---|
OPTIONS returns 404 |
The route or server does not handle OPTIONS | Add OPTIONS handling at the server, framework, gateway, or hosting layer |
OPTIONS returns 405 |
The route exists but OPTIONS is not allowed | Allow OPTIONS for CORS preflight before route logic blocks it |
OPTIONS returns 401 or 403 |
Auth middleware is blocking preflight | Let preflight pass without requiring the bearer token or session cookie |
| Preflight succeeds but real request fails | CORS permission is probably okay, now debug the actual API request | Check method, URL, token, JSON body, validation, and server logs |
| Preflight redirects | The API may be redirecting HTTP to HTTPS, API to login, or old path to new path | Use the final HTTPS API URL directly and avoid auth redirects on API routes |
Important: Do not debug the JSON body first if the preflight request is failing. Your JSON body has not reached the route yet. Fix the browser permission layer first, then debug the real request.
Authorization and Content-Type can change browser behavior
Two headers show up again and again in this problem: Authorization and Content-Type. They are normal API headers, but they can also cause the browser to use preflight for cross-origin requests.
Authorization header mistakes
In Postman, you may use the Authorization tab and it quietly formats the token correctly. In browser code, you must build the header yourself. A tiny mismatch can cause a 401, 403, or a preflight failure.
Good fetch example:
const response = await fetch("https://api.example.com/v1/profile", {
method: "GET",
headers: {
"Authorization": "Bearer YOUR_TOKEN",
"Accept": "application/json"
}
});
Common mistakes:
- Using
BearerYOUR_TOKENwithout the space. - Forgetting the word
Bearerwhen the API expects it. - Using a staging token against production.
- Copying a token with a hidden newline or extra space.
- Sending the token in the browser when the API key should stay server-side.
- Preflight failing because the server forgot
AuthorizationinAccess-Control-Allow-Headers.
If the browser sends this preflight:
Access-Control-Request-Headers: authorization
Then the API should allow it in the preflight response:
Access-Control-Allow-Headers: Authorization
Content-Type mistakes
Postman can send JSON correctly because you selected “raw JSON” or used its body editor. In browser code, you need both the right header and the right body serialization.
Correct JSON request:
const response = await fetch("https://api.example.com/v1/orders", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
},
body: JSON.stringify({
productId: 42,
quantity: 1
})
});
Wrong version that often creates confusing backend errors:
const response = await fetch("https://api.example.com/v1/orders", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: {
productId: 42,
quantity: 1
}
});
That bad version does not send the object as JSON. It sends the wrong thing because the object was not stringified. Depending on the backend, you might see a 400, 415, 422, empty body, or validation error.
Rule: If you set Content-Type: application/json, send JSON.stringify(data). If you use FormData, usually do not manually set multipart/form-data; let the browser set the boundary.
HTTPS, mixed content, and redirects can break browser requests
A browser page loaded over HTTPS should not casually call insecure HTTP APIs. If your site is secure but the API URL starts with http://, the browser may block the request as mixed content or upgrade/block parts of the request depending on resource type and browser behavior.
This can happen when:
- Your local API runs on
http://localhost:8000but your deployed frontend runs on HTTPS. - Your API documentation shows an old
http://endpoint. - The API redirects from HTTP to HTTPS and the preflight does not survive the redirect cleanly.
- A WordPress site uses HTTPS but a custom script calls an HTTP API URL.
Use the final HTTPS endpoint directly when testing from the browser:
https://api.example.com/v1/orders
Instead of relying on this redirect chain:
http://api.example.com/v1/orders
301 redirect to https://api.example.com/v1/orders
Debugging shortcut: In Chrome or Edge DevTools, open the Network tab, click the failed request, and check whether the failed row is the OPTIONS request, the real request, or a redirect. That one detail tells you which layer to fix first.
The practical debugging order
When beginners debug this issue randomly, they waste hours changing headers, rewriting fetch code, reinstalling packages, switching Axios to fetch, or blaming React. Use this order instead.
Confirm the exact request that works in Postman
Copy the working method, URL, headers, body, and auth settings. Do not rely on memory. Export or copy the cURL command if possible. Check whether Postman is automatically adding headers, cookies, or auth values.
Convert the working request carefully
Translate the cURL or Postman request into fetch, Axios, PHP, or WordPress code without changing the method, endpoint, auth format, body type, or header names. This is where many bugs start.
Open the browser Network tab
Do not only read the console error. The console often summarizes the symptom. The Network tab shows whether the browser sent a preflight, what status it received, what headers were requested, and whether the real request was sent.
If OPTIONS failed, fix CORS first
Check Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers, and whether auth middleware is blocking OPTIONS. Do not debug the JSON body until preflight succeeds.
If OPTIONS succeeded, debug the real request
Now check the actual status code: 400, 401, 403, 404, 415, 422, 429, 500, 502, or 503. Look at the response body, route logs, validation errors, and backend parser configuration.
If cookies are involved, debug credentials separately
Check credentials: "include", cookie domain, SameSite, Secure, CORS credentials headers, and whether the API response uses a wildcard origin.
Compare local and production environments
A request can work locally and fail after deployment because the allowed origin changed from http://localhost:5173 to https://yourdomain.com. Update the server allowlist, not just the frontend code.
Working cURL vs browser fetch example
Suppose this cURL request works:
curl -i -X POST "https://api.example.com/v1/orders" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
--data '{"productId":42,"quantity":1}'
A matching browser fetch request would look like this:
async function createOrder() {
const response = await fetch("https://api.example.com/v1/orders", {
method: "POST",
headers: {
"Authorization": "Bearer YOUR_TOKEN",
"Content-Type": "application/json",
"Accept": "application/json"
},
body: JSON.stringify({
productId: 42,
quantity: 1
})
});
const data = await response.json();
if (!response.ok) {
console.error("API error:", response.status, data);
return;
}
console.log("Order created:", data);
}
But this browser version can still fail even if the cURL works. Why? Because the browser may first ask the API this question:
Can http://localhost:5173 send a POST request to /v1/orders
with Authorization and Content-Type headers?
The server must answer yes with CORS headers. If it does not, the browser blocks the flow before the JavaScript gets a readable API response.
Server headers needed for this example
Access-Control-Allow-Origin: http://localhost:5173
Access-Control-Allow-Methods: POST, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type, Accept
For production, do not leave your local origin hardcoded. You may need to allow your real frontend domain:
Access-Control-Allow-Origin: https://yourfrontenddomain.com
Do not put private API keys directly in frontend JavaScript. If the API key is secret, call the API from your backend, serverless function, or WordPress/PHP server code. Browser users can inspect frontend code, network requests, and exposed tokens.
Bad fixes that hide the real problem
When developers search this error online, they often find shortcuts that seem to make the console message disappear. Many of those shortcuts are not real fixes.
| Bad fix | Why it is bad | Better fix |
|---|---|---|
Using mode: "no-cors" |
It can give you an opaque response that JavaScript cannot read, so your app still cannot use the JSON. | Fix server CORS headers or call the API from your backend. |
Adding Access-Control-Allow-Origin to request headers |
That is a response header. The browser needs it from the server response. | Configure the API, reverse proxy, hosting layer, or server framework. |
| Installing a CORS browser extension | It only changes your local browser behavior and does not fix real users, production, or security. | Test the actual headers your users will receive. |
| Allowing every origin with credentials | Private APIs should not blindly expose credentialed responses to arbitrary origins. | Use an allowlist of trusted frontend origins. |
| Moving secrets into React environment variables | Frontend environment variables are usually bundled into client code and can be exposed. | Keep private keys on the server side. |
Special note for WordPress users
This problem also appears in WordPress. A Custom HTML block, theme script, shortcode, plugin snippet, or page builder widget may call an API from the visitor’s browser. If that API does not allow your WordPress site origin, the request can fail even though it works in Postman.
For public APIs with browser support, frontend JavaScript may be fine. For private keys, paid APIs, admin APIs, or APIs that need secret credentials, use server-side WordPress code instead. That usually means calling the API with wp_remote_get(), wp_remote_post(), or wp_remote_request() from PHP rather than exposing the key in browser JavaScript.
Example WordPress-style server-side request:
$response = wp_remote_post('https://api.example.com/v1/orders', array(
'headers' => array(
'Authorization' => 'Bearer YOUR_SERVER_SIDE_TOKEN',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
),
'body' => wp_json_encode(array(
'productId' => 42,
'quantity' => 1,
)),
'timeout' => 20,
));
Notice that the token belongs on the server side in this example. If you paste a secret token into public page JavaScript, visitors may be able to find it.
Common browser errors and what they usually mean
| Error or symptom | Likely area | First thing to inspect |
|---|---|---|
No 'Access-Control-Allow-Origin' header |
CORS response missing or wrong | API response headers for the browser origin |
Request header field authorization is not allowed |
Preflight headers | Access-Control-Allow-Headers |
Method POST is not allowed by Access-Control-Allow-Methods |
Preflight methods | Access-Control-Allow-Methods |
TypeError: Failed to fetch |
Network, CORS, mixed content, DNS, TLS, blocked request, or extension | Network tab details, not only the console |
| 401 in browser but works in Postman | Token format, token environment, cookie, or preflight auth block | Authorization header and whether OPTIONS is being rejected |
| 415 Unsupported Media Type | Content-Type/body mismatch | Header, body serialization, backend parser |
| Empty request body on server | Body not stringified, wrong parser, wrong Content-Type, or missing middleware | Raw request payload and backend body parser |
| Works locally but fails after deployment | Origin allowlist, HTTPS, environment variable, redirect, or production token | Actual deployed frontend origin and API base URL |
Copyable debugging note template
Before asking for help, collect the real clues. A clear note saves time because it separates “the API is broken” from “the browser blocked the request before the API route ran.”
API browser debugging note
Frontend origin:
Example: http://localhost:5173
API URL:
Example: https://api.example.com/v1/orders
Method:
POST
Works in Postman or cURL?
Yes / No
Browser error message:
Paste exact console error here.
Network tab:
- Is there an OPTIONS request? Yes / No
- OPTIONS status code:
- Actual request status code:
- Did the real request get sent? Yes / No
Request headers I am trying to send:
Authorization
Content-Type
Accept
Request body type:
JSON / FormData / query string / no body
Auth type:
Bearer token / cookie session / API key / none
CORS response headers received:
Access-Control-Allow-Origin:
Access-Control-Allow-Methods:
Access-Control-Allow-Headers:
Access-Control-Allow-Credentials:
What changed recently:
New domain / local to production / new token / backend update / HTTPS change / plugin change
Related CodeZips tools for this problem
Use these tools when you want to turn the debugging process into a clearer checklist, request comparison, or support note.
CORS Preflight Request Explainer and Fix Checklist Generator
Use this when the browser sends an OPTIONS request or shows a CORS/preflight error. It helps you identify the missing origin, method, header, credential, or OPTIONS handling issue.
API Authentication Header Builder and Debug Checklist Generator
Use this when the request needs a bearer token, API key, basic auth, or another auth header and you are unsure whether the header is formatted correctly.
API Content-Type and Accept Header Explainer and Fix Checklist Generator
Use this when the API returns 400, 406, 415, 422, empty body, or validation errors after moving from Postman to browser code.
API Request Troubleshooting Checklist Generator
Use this when you are not sure whether the failure is URL, method, auth, headers, CORS, JSON body, timeout, status code, or backend logic.
HTTP Status Code Debugging Helper
Use this when the browser shows a real status code such as 400, 401, 403, 404, 405, 415, 422, 429, 500, 502, or 503.
cURL to JavaScript, PHP and WordPress Converter
Use this when the API documentation gives a working cURL command and you need to convert it into fetch, PHP cURL, or WordPress HTTP API code.
API Error Response Explainer and Debug Checklist
Use this when the API returns a JSON error body and you need to understand whether the problem is auth, validation, content type, permission, route, or server logic.
Technical references used for this guide
This article is written as a practical beginner debugging workflow, but the browser behavior is based on standard web platform documentation.
FAQ
Why does my API work in Postman but not in browser fetch?
Postman sends direct HTTP requests. Browser fetch must follow browser security rules, including CORS, preflight, credential handling, and mixed-content blocking. Your request can be valid but still blocked because the API server did not allow your browser origin.
Is CORS a frontend problem or backend problem?
The symptom appears in the frontend, but the permission usually has to be fixed on the server response. Frontend code cannot force the browser to accept a cross-origin response that the server did not allow.
Why does the browser send an OPTIONS request before my POST request?
That OPTIONS request is a CORS preflight. The browser is asking whether the real request is allowed from the current origin with the requested method and headers. If the preflight fails, the real request may not be sent.
Should I use mode: “no-cors” to fix the error?
Usually no. For API work, no-cors often gives JavaScript an opaque response that it cannot read. It hides the useful error instead of giving your app usable JSON.
Why does adding Authorization cause a CORS error?
The Authorization header can trigger a preflight request in cross-origin browser calls. The server must allow that header in Access-Control-Allow-Headers. If it does not, the browser blocks the request flow.
Why does application/json cause preflight?
JSON API requests often use headers and body types that make the browser check permission first. The API must handle OPTIONS and allow the requested headers, including Content-Type when needed.
Can I put my API key in React or browser JavaScript?
Do not put private API keys in browser JavaScript. Browser code can be inspected. For private keys, use a backend route, serverless function, or WordPress/PHP server-side request.
What should I check first when browser fetch fails?
Open the browser Network tab. Check whether the failed request is OPTIONS or the real API request. If OPTIONS failed, debug CORS/preflight. If OPTIONS succeeded and the real request failed, debug the actual status code, headers, body, token, and server logs.