The Problem: Authentication Failures on Private Routes
Building a headless WordPress site or a mobile app often goes smoothly until you move beyond simple GET requests. Everything works fine when fetching public posts. However, the second you try to update a user or create a draft, you hit a wall. Even with valid credentials, the server rejects the request with a rest_forbidden error.
{"code":"rest_forbidden","message":"Sorry, you are not allowed to do that.","data":{"status":401}}
This error typically means WordPress either doesn't recognize your identity or doesn't believe you have the necessary permissions. This happens to administrators too. Usually, the culprit is a server configuration that strips away your credentials before they reach WordPress.
Debug Process: Identifying the Bottleneck
Don't start changing your application code until you know where the communication is breaking down. Follow these steps to isolate the issue:
- Analyze the Status Code: A 401 status is an identity crisis; the server doesn't know who you are. A 403 status is a permission crisis; the server knows you, but you lack the required 'capability' (like
edit_posts). - Isolate with Postman: Attempt a POST request using Basic Auth and an Application Password in Postman. If this works but your app fails, your app is likely failing to send the
Authorizationheader correctly. - Verify Header Arrival: Create a
test.phpfile in your root directory containing<?php print_r(getallheaders()); ?>. If theAuthorizationkey is missing from the output when you call it, your web server is stripping it.
Solution 1: Use Native Application Passwords
Since version 5.6, WordPress includes a built-in system for API authentication. You no longer need to install third-party Basic Auth plugins. Using your actual admin password for API calls will fail by default because it is insecure and blocked.
- Navigate to Users > Profile in your dashboard.
- Find the Application Passwords section near the bottom.
- Enter a name like "NextJS Frontend" and click Add New Application Password.
- Save the 24-character string immediately; you won't see it again.
When making calls, use your username and this 24-digit string as your credentials. If you need to generate high-entropy keys for other parts of your stack, a Password Generator can help, but for this specific task, stick to what WordPress provides.
Solution 2: Fixing Apache Header Stripping
Apache is the most common source of 401 errors. Many shared hosts and default configurations strip the Authorization header to prevent credential leakage. Consequently, WordPress receives the request but sees no login info.
Open your .htaccess file and insert these lines at the very top, before the # BEGIN WordPress block:
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTP:Authorization} ^(.*)
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{1}]
</IfModule>
This directive captures the header and passes it into the $_SERVER['HTTP_AUTHORIZATION'] variable. Once this is in place, WordPress can finally see your Application Password.
Solution 3: Adjusting Nginx Configuration
Nginx usually passes headers reliably, but specific FastCGI setups can drop them. If you are running on Nginx, check your site configuration file—usually located in /etc/nginx/sites-available/. Ensure your PHP block includes the following line:
fastcgi_pass_header Authorization;
After saving the file, test your config with sudo nginx -t and reload the service using sudo systemctl reload nginx.
Solution 4: Authenticating Local AJAX Requests
If your script lives inside the WordPress ecosystem—such as a custom plugin or a Gutenberg block—don't use Application Passwords. Instead, use a Nonce. This is more secure for front-end scripts because it ties the session to the currently logged-in user.
First, pass the nonce from PHP to your JavaScript:
wp_localize_script( 'my-handle', 'wpSettings', [
'nonce' => wp_create_nonce( 'wp_rest' )
]);
Then, include the X-WP-Nonce header in your JavaScript fetch call:
fetch('/wp-json/wp/v2/posts/42', {
method: 'POST',
headers: {
'X-WP-Nonce': wpSettings.nonce,
'Content-Type': 'application/json'
},
body: JSON.stringify({ status: 'publish' })
});
Verifying the Connection
Test your fix using curl. This eliminates browser-related issues like CORS or cached headers. Run this command in your terminal:
curl -X POST https://your-site.com/wp-json/wp/v2/posts \
-u "username:xxxx-xxxx-xxxx-xxxx-xxxx-xxxx" \
-H "Content-Type: application/json" \
-d '{"title":"Connection Test","status":"draft"}'
A successful fix will return a 201 Created status code. If you receive a 403, verify that your user account has the "Edit Posts" role. If you are managing server files and need to check folder permissions, a Unix Permissions Calculator is useful for ensuring your wp-content folder isn't locked down too tightly (usually 755).
Summary of Key Takeaways
- Server over Code: 90% of REST API 401 errors are caused by Apache or Nginx stripping headers.
- Check Security Plugins: Tools like Wordfence or 'All In One WP Security' can disable the REST API entirely. Check their settings if manual fixes fail.
- Capabilities Matter: Authentication (who you are) is not Authorization (what you can do). Ensure your user has the correct WordPress Role.

