Why Your API Works in Postman but Fails in Browser

API Debugging Guide for Beginners

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.

CORS OPTIONS preflight Authorization headers Content-Type Cookies HTTPS and mixed content fetch vs cURL

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_TOKEN without the space.
  • Forgetting the word Bearer when 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 Authorization in Access-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.

Cookies, sessions, and credentials are a separate layer

Some APIs do not use bearer tokens. They use login cookies or server sessions. This can become confusing because Postman may store cookies differently from your browser, and browser fetch does not always send cookies the way beginners expect.

For fetch, credential behavior matters:

const response = await fetch("https://api.example.com/account", {
  method: "GET",
  credentials: "include"
});

But credentials: "include" is only one side of the problem. The server must also allow credentialed cross-origin access. A typical credentialed CORS response needs a specific origin and credentials permission:

Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Vary: Origin

Do not combine credentialed requests with this:

Access-Control-Allow-Origin: *

That wildcard pattern is a common reason a browser blocks a response even though the request appears to work elsewhere.

Cookie settings to check

  • Domain: Is the cookie valid for the frontend and API domain relationship you are using?
  • SameSite: Cross-site cookie behavior depends on this setting.
  • Secure: Cookies meant for cross-site HTTPS flows often need secure transport.
  • Credentials option: Browser fetch needs the correct credentials value.
  • CORS response: Credentialed responses need the correct server-side CORS headers.
  • Third-party cookie restrictions: Browser privacy settings can affect third-party cookies.

If Postman succeeds because it has a cookie but your browser fails because the cookie is not sent, this is not a JSON problem. It is a credential transport problem.

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:8000 but 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.

1

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.

2

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.

3

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.

4

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.

5

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.

6

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.

7

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.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top