How to Fix 404 and 403 Errors from Your Site's REST API
When your own REST API answers with 404 Not Found or 403 Forbidden, the message is short but the causes are many. A 404 can mean a mistyped route, a missing prefix, a rewrite rule that never reaches the application or the wrong HTTP method. A 403 can come from the application refusing the credentials, a security plugin, a web application firewall or the web server itself. The fastest route to a fix is to work out which layer sent the response, then fix that layer. This guide shows how to reproduce the error with curl and Postman, read the response to tell the layers apart, and fix the common causes, with particular attention to the WordPress REST API.
What 404 and 403 mean in an API
In a REST API the status code is the first thing a client reads, so it pays to know exactly what each one claims:
| Code | What the server is saying | Typical API meaning |
|---|---|---|
404 Not Found | Nothing exists at this address | Unknown route, wrong prefix or version, missing record, or a route that exists only for another method |
403 Forbidden | I know who you are, or I do not care, and the answer is no | Valid route, but the caller lacks permission, or a firewall or server rule blocked the request |
401 | I do not know who you are | Missing, expired or malformed credentials |
405 Method Not Allowed | The route exists but not for this method | POST sent to a GET-only route; many frameworks send 404 instead |
The important question is who sent the response. A 404 or 403 can come from your application code, from the web server before the application ever runs, from a CDN or firewall in front of the server, or from a security plugin inside the application. Each layer leaves different fingerprints in the body and headers, and each needs a different fix. A generic HTML error page from nginx and a JSON error object from your API framework both carry a 404, but they point to completely different problems.
Some APIs deliberately answer 404 instead of 403 when a caller is not allowed to see a record, so as not to reveal that it exists. If your own API does that, a 404 for an existing record may really be a permission problem.
The common causes, layer by layer
| Layer | Causes of 404 | Causes of 403 |
|---|---|---|
| Client request | Wrong base address, typo in the path, missing version such as /v1/, wrong method, trailing slash mismatch | Missing or wrong credentials, wrong auth type, expired token, API key in the wrong header |
| CDN or firewall | Rare; a rule returning a block page that looks like a 404 | WAF rule matching the body, user agent or country; bot protection challenge; rate limiting set to 403 |
| Web server | Rewrite rules not passing the path to the application; location block not matching | deny rules, directory permissions, ModSecurity, blocked file types |
| Application | Route not registered, router prefix differs, record ID not found | Permission checks fail, missing nonce, role lacks the capability, security plugin blocks the API |
Work from the outside in. If the request never reaches your application, no amount of changing application code will help, and the same goes in reverse. The 403 errors guide and the 404 errors guide cover these codes for normal web pages, which share many of the server-level causes.
How to reproduce the error with curl and Postman
Start with curl, because it sends exactly what you type and shows every header in the response:
curl -i -X GET https://www.example.com/wp-json/wp/v2/posts?per_page=1
The -i flag prints the status line and headers above the body. Read three things:
- The body. A JSON error with a code field came from the application. A plain HTML page came from the web server, CDN or firewall.
- The
Serverheader.cloudflarewith a block page means the CDN answered.nginxorApachewith an HTML page means the web server did. - Any request ID header, such as
cf-ray. Use it to find the event in your CDN or firewall logs.
To send credentials with curl, use -u for a username and password, or --oauth2-bearer for a bearer token:
curl -i -u "editor:abcd efgh ijkl mnop qrst uvwx" \
-X POST https://www.example.com/wp-json/wp/v2/posts \
-H "Content-Type: application/json" \
-d '{"title":"Test","status":"draft"}'
curl -i --oauth2-bearer "$API_TOKEN" https://api.example.com/v1/orders/1042
404 error in Postman
When Postman shows a 404, check the request it really sent, not the one you think it sent. Open the Postman Console from the bottom bar and look at the full address. Common faults are an environment variable such as {{baseUrl}} that is empty or points at staging, a missing /wp-json or /api/v1 prefix, a double slash where two variables join, and the wrong method in the dropdown.
403 Forbidden error in Postman
For a 403, check the request's auth settings tab. A request set to inherit from its collection sends nothing if the collection has no auth set. Check the type matches what the API expects, and that the token has not expired. If curl succeeds with the same credentials and Postman fails, compare headers in the console. Some firewalls block the default Postman user agent string.
WordPress REST API 404 and 403 errors
The WordPress REST API lives under /wp-json/. A request to https://www.example.com/wp-json/ should return a JSON index of the available routes. If it returns an HTML 404 page, WordPress never saw the request.
wp-json returns 404
- Plain permalinks or missing rewrites. Go to Settings, then Permalinks, and click Save Changes without changing anything. That regenerates the rewrite rules. On Apache, check
.htaccessstill holds the standard WordPress block. - nginx without a WordPress location rule. The main location must pass unknown paths to WordPress:
location / {
try_files $uri $uri/ /index.php?$args;
}
A quick test that bypasses pretty permalinks is the query form of the route. If this works while /wp-json/ fails, the problem is rewriting, not the API:
curl -i "https://www.example.com/?rest_route=/wp/v2/posts&per_page=1"
A JSON response with the code rest_no_route means WordPress received the request but no route matches the path and method. Check the namespace and version in the path, the HTTP method, and that the plugin registering a custom route is active.
WordPress REST API returns 403 or 401
- Not logged in. Requests that create or edit content need credentials. WordPress usually answers 401 when no user is identified and 403 when the user lacks the capability.
- Cookie requests without a nonce. JavaScript on your own site using the login cookie must also send a REST nonce in the
X-WP-Nonceheader. Without it WordPress treats the request as logged out and returns a 401; an invalid nonce givesrest_cookie_invalid_nonce. - Application Passwords not reaching PHP. On some Apache set-ups with PHP running as CGI or FastCGI, the credentials header is stripped before PHP sees it. On Apache 2.4.13 or later, add
CGIPassAuth Onto the site's.htaccessor virtual host. - Security plugins. Many security plugins can restrict the REST API to logged-in users, or block routes such as
/wp/v2/users. Check their settings before changing server rules.
See the WordPress HTTP errors guide for other status codes from the same site.
Firewall, server and framework causes
Web application firewalls
A WAF inspects each request and can block API calls whose JSON body contains strings that look like SQL or script injection, or whose user agent looks automated. The block usually comes back as a 403 HTML page. On Cloudflare, open Security, then Events, and search by the cf-ray value from the response to see which rule fired. On Plesk with ModSecurity, check the web application firewall log and the domain error log for the rule ID. Then write a narrow exception for that rule on your API path, rather than switching the firewall off. The Plesk HTTP errors guide shows where those settings live.
Web server rules
Check the access and error logs for the exact request:
sudo grep ' /api/v1/orders' /var/log/nginx/access.log | tail -5
sudo tail -20 /var/log/nginx/error.log
An entry in the access log with 403 and an error log line mentioning access forbidden by rule means an nginx deny rule matched. A 404 in the access log with no matching application log means the location block did not hand the request to the application.
Application frameworks
In Node.js with Express, a route that exists only for GET answers a POST with a 404 and a body such as Cannot POST /api/v1/orders. The same body appears when a router is mounted at a different prefix from the one the client uses. List your registered routes and compare them with the request. The Node.js HTTP errors guide covers error handling in more depth.
A worked example
Example Bakery's order form posted to /wp-json/bakery/v1/orders. It worked in Postman, but the live form failed. The browser's Network panel showed a 403 with an HTML body and a Server: cloudflare header, so the application was never reached.
Searching Cloudflare Security Events by the cf-ray ID showed a managed WAF rule matching the order notes field, which contained an apostrophe followed by the word or. The fix was a WAF exception that skipped that one rule for that one path. The next request returned a 401 instead, this time as JSON from WordPress, because the form's JavaScript was not sending the X-WP-Nonce header. Adding the nonce brought a 201 Created.
Two layers, two different fixes. Reading the body and headers first saved hours of changing code in the wrong place.
The fix, step by step
Step 1
Reproduce the failing request with curl
Copy the exact address, method, headers and body of the failing call and send it with curl -i. Include credentials with -u or --oauth2-bearer if the call needs them. Note the status line, the Server header, any request ID header such as cf-ray, and whether the body is JSON or HTML. If curl succeeds where your app fails, the difference lies in what the app sends, so compare the two requests line by line.
Step 2
Identify which layer answered
Use the response to decide who sent it. A JSON error with a code field came from your application. An HTML block page with a CDN server header came from the CDN or firewall. A plain nginx or Apache error page came from the web server. Check the access log with sudo grep for the request path. If the request appears there but not in the application log, the web server stopped it.
Step 3
Fix the route for a 404
Compare the requested path and method with the routes your application registers. Check the base address, version prefix, trailing slash and method. For WordPress, request /wp-json/ to see the route index, and try ?rest_route= to bypass rewriting. If the query form works, resave permalinks or fix the nginx try_files rule. Confirm the corrected request returns 200 and a JSON body from the application.
Step 4
Fix the credentials for a 401 or 403
Confirm the API expects the auth type you are sending, that the token or password is current and that the user has the permission the route needs. For WordPress Application Passwords on Apache with CGI or FastCGI, add CGIPassAuth On so the credentials reach PHP. For browser calls using the login cookie, send the REST nonce in X-WP-Nonce. Retry with curl and confirm the error changes or clears.
Step 5
Check the firewall and security plugins
If the 403 is an HTML block page, find the event in your CDN or firewall log using the request ID, and note the rule that fired. Add a narrow exception for that rule on that API path rather than switching protection off. In WordPress, check security plugins for settings that restrict the REST API to logged-in users. Re-run the request and confirm the response now comes from the application.
Step 6
Test in Postman and the browser
Build the corrected request in Postman, using an environment variable for the base address. Open the Postman Console and confirm the full address, method and headers it actually sends. Then trigger the call from your site and watch the DevTools Network panel. If the browser alone fails, check the Console for a CORS error and make sure OPTIONS preflight requests return a success status with the right access control headers.
Step 7
Purge caches and retest
If a CDN sits in front of the API, purge the cached responses for the affected path, because an old 404 or 403 can be served after the fix. Add Cache-Control: no-store to API responses that change per user. Run the curl request again, then the Postman request, then the browser call, and confirm all three return the expected success status and JSON body.
Step 8
Add monitoring
Create a simple health route that returns 200, and check it from outside your network every few minutes with an uptime monitor. Save a Postman collection with one working request per route and method, and run it after every server, plugin or firewall change. Log request IDs in the application. Next time an API returns 404 or 403, you can match it to a log line and a layer within minutes.
Edge cases worth knowing
- Preflight requests. Browsers send an
OPTIONSrequest before many cross-origin calls. If your server or firewall answers it with 403 or 404, the real request is never sent. AllowOPTIONSon API routes. - Cached errors. A CDN can cache a 404 or 403 from an earlier broken deployment. Send
Cache-Control: no-storeon API responses that should not be cached, and purge after fixing. - Redirects that change the method. An http to https or trailing slash redirect can turn a POST into a GET, which then hits a GET-only route and returns 404. Call the final address directly.
- Bot rules. Rules written to block scrapers or AI crawlers can catch your own integrations. Check the AI crawler access guide if you have such rules.
- Rate limits. The correct code is 429, but some firewalls use 403. A 403 that appears only after many fast calls is probably a rate limit.
Keeping the API reliable
- Add a health route such as
/api/v1/healththat returns 200, and monitor it from outside your network so you hear about rewrite and firewall problems first. - Return JSON errors with clear codes from the application, so any HTML error page is instantly recognisable as coming from another layer.
- Keep a saved Postman collection with one working request per route and method, and run it after server, plugin or firewall changes.
- Log request IDs in the application and pass them back in a response header, so you can match a failed call to a log line in seconds.
- Review firewall exceptions every few months and keep them as narrow as possible: one rule, one path.
1. How do I fix a 404 error in a REST API?
You fix a REST API 404 by finding why the server has nothing at that path for that method. Send the request with curl to see who answered, then check the base address, version prefix, trailing slash and HTTP method against the routes your application registers. If the web server returns an HTML 404 before the application runs, fix the rewrite rules. For WordPress, resave permalinks or correct the nginx try_files rule so requests under wp-json reach WordPress.
2. Why do I get a 404 error in Postman?
You get a 404 in Postman when the address it actually sends does not match a route on the server. The usual causes are an empty or wrong environment variable for the base address, a missing prefix such as wp-json or api/v1, a double slash where variables join, a typo in the path or the wrong method in the dropdown. Open the Postman Console to see the exact request that was sent, then compare it with a request that works in curl.
3. How do I fix a 403 Forbidden error in Postman?
You fix a 403 in Postman by sending credentials the server accepts and ruling out a firewall block. Check the auth settings tab on the request and the parent collection, confirm the auth type and token are correct and current, and make sure the user has permission for that route. If the response is an HTML block page rather than JSON, a web application firewall stopped it, so find the event in its log and add a narrow exception.
4. What is the difference between a 401 and a 403 error in an API?
A 401 means the server does not know who you are, while a 403 means it knows or does not care, and still refuses. In practice, 401 points to missing, expired or malformed credentials, so fix the token or login. A 403 points to permissions or a block: the user lacks the role or capability, a security plugin restricts the route, or a firewall or server rule stopped the request. WordPress uses both in exactly this way for REST API calls.
5. Why does my WordPress REST API return 404?
Your WordPress REST API returns 404 when requests under wp-json never reach WordPress, or when no registered route matches the path and method. If you get an HTML 404 page, resave the permalink settings and check the Apache rewrite block or the nginx try_files rule. If you get a JSON error with the code rest_no_route, WordPress saw the request, so check the namespace, version, method and that the plugin registering the route is active.
6. Why does my API work in Postman but not in the browser?
An API that works in Postman but fails in the browser is usually blocked by CORS, which is a browser rule rather than a server error. The browser may send an OPTIONS preflight request first, and if the server or firewall answers it with 403 or 404, the real request is never sent. Check the DevTools Console for a CORS message, allow OPTIONS on the API routes and return the right access control headers for your site's origin.
7. Can a firewall cause a 403 error on my API?
Yes, a web application firewall is one of the most common causes of a 403 on an API. It may block requests whose JSON body looks like an injection attempt, whose user agent looks automated, or that arrive too quickly. The giveaway is an HTML block page instead of your API's usual JSON error. Use the request ID from the response headers to find the event in the firewall log, then add an exception for that rule on that path only.
The HTTP error audit requests your pages and endpoints, records each status code and the server that answered, and flags 404 and 403 responses so you can trace them to the right layer.
Open HTTP Error Fixes โ