Blue laptop displaying the WordPress logo, representing WordPress REST API authentication

WordPress REST API 401 Error: Fix Authentication

A WordPress REST API 401 error means the request arrived at WordPress without valid authentication: either no credentials were sent, the Authorization header was stripped before reaching PHP, the nonce expired, or a security plugin blocked the route before WordPress could process it. The most common cause is the server stripping the Authorization header, which accounts for the majority of silent 401 failures on external integrations using application passwords. If you are debugging a rest_cannot_create error specifically, our guide to fixing rest_cannot_create covers that error code in depth. This post focuses on the 401 itself: why it happens, how to fix each cause, and how to confirm the fix holds.

What does a WordPress REST API 401 error mean?

A 401 Unauthorized response from the WordPress REST API means WordPress did not recognize the request as coming from an authenticated user. The request was treated as anonymous (user ID 0), and the endpoint required authentication to proceed. This is distinct from a 403 Forbidden, which means the user was authenticated but lacks the capability for that specific action. If you are seeing 403, the problem is roles and capabilities, not authentication. Skip to Cause 6 below.

One subtlety: the rest_cookie_invalid_nonce error returns HTTP 403, not 401, even though it is an authentication failure. WordPress considers the cookie valid (the user is logged in) but the nonce invalid, so it returns 403 with the message “Cookie check failed.” Developers often search for “401” when they see this error because the behavior feels like an auth failure. It is, but the HTTP status code is 403. Both cases are covered below.

Cookie auth or application-password auth: which path are you on?

Before you fix anything, identify which authentication method your request uses. The diagnostic path diverges completely depending on the answer. WordPress supports two built-in REST API authentication methods, and they fail for different reasons.

Cookie authentication (logged-in user in browser)

Cookie auth is the default method for requests made from within WordPress: plugins, themes, the block editor, wp-admin AJAX. The user is already logged in, their session cookies are sent automatically by the browser, and WordPress verifies a nonce to prevent CSRF. The nonce is created with wp_create_nonce( 'wp_rest' ) and sent either as the X-WP-Nonce header or the _wpnonce query/body parameter. If no nonce is present, WordPress sets the current user to 0, making the request anonymous even though the user is logged in. If the nonce is present but invalid or expired, WordPress returns rest_cookie_invalid_nonce with HTTP 403.

Cookie auth only works same-origin. The request must come from the same domain where WordPress is installed, and the browser must send the session cookies. Cross-origin requests from another domain, a mobile app, or a server-side script cannot use cookie auth. They need application passwords instead.

Application password authentication (external script or tool)

Application passwords use HTTP Basic Authentication. The client sends an Authorization: Basic base64(username:password) header with every request, where the password is a generated application password (not the user’s login password). No nonce is required. No cookies are required. The request can come from any origin: a cron job, a Python script, a Node app, a mobile client. WordPress validates the credentials on each request independently, with no session state between requests.

Application passwords require HTTPS. WordPress checks is_ssl() and silently disables the feature on plain HTTP. For a deeper dive on application password failures, see our application password troubleshooting guide.

Auth MethodCredential SentNonce RequiredRequires HTTPSWorks Cross-Origin
Cookie authSession cookies + X-WP-NonceYes (wp_rest action)NoNo (same-origin only)
Application passwordAuthorization: Basic headerNoYesYes

Why does GET work but POST returns 401?

This is the signature symptom of the most common 401 cause. GET requests to public endpoints like /wp-json/wp/v2/posts do not require authentication, so they succeed even when credentials are missing. POST, PUT, and DELETE require authentication, so they fail with 401 when the credentials never arrive. If you see this pattern, the Authorization header is being stripped by your server. Jump to Cause 2.

Here is the full diagnostic table. Match your symptom to the likely cause, then jump to the corresponding section.

SymptomHTTP StatusLikely CauseJump To
GET works, POST fails401Authorization header stripped by serverCause 2
Request from browser, no X-WP-Nonce sent401Missing nonce in cookie authCause 3
rest_cookie_invalid_nonce in response403Nonce expired or invalidCause 3
All requests fail, even public GET401rest_authentication_errors filterCause 4
Worked before installing a security plugin401 or 403Security plugin or WAF blocking RESTCause 5
Auth succeeds (users/me returns user) but writes fail403User role lacks capabilitiesCause 6
Fetch works in XHR, fails in fetch()401Cookies not sent (credentials option)Cause 3
Site on http://, application passwords missing401HTTPS required for app passwordsCause 2

Cause 1: No authentication credentials sent with the request

The simplest cause: your client is not sending any authentication at all. This happens when a developer assumes the REST API is open by default, or when a JavaScript fetch call omits the nonce header, or when a server-side script forgets to include the Authorization header. GET requests to public endpoints will succeed, giving the false impression that the API is working. The moment you try a write operation, you get 401.

How to confirm it

For application-password auth, run this curl command:

curl -i -u "your_username:your_app_password" https://example.com/wp-json/wp/v2/users/me

If this returns the user object with the correct ID, authentication is working and the problem is elsewhere. If it returns 401 with rest_cannot_view_user, credentials are not arriving. Check whether you are sending the header at all.

For cookie auth, check the browser’s Network tab in DevTools. Look for the X-WP-Nonce header in the request. If it is absent, your JavaScript is not sending the nonce. If the response includes a X-WP-Nonce header in the response (WordPress sends a refreshed nonce on successful cookie-authenticated requests), compare it against what your client is sending.

The fix

For application-password auth, ensure every request includes the Authorization header. In curl, use the -u flag. In code, construct the header manually:

Authorization: Basic $(echo -n 'username:application_password' | base64)

The word “Basic” must be followed by a space, then the base64-encoded username:password string. A missing space is a common cause of silent failure.

For cookie auth in JavaScript, use the built-in wp.api client which handles nonce injection automatically. If you are making manual requests, use wp_localize_script to pass the nonce to your script, per the WordPress REST API Handbook:

wp_localize_script( 'wp-api', 'wpApiSettings', array(
 'root' => esc_url_raw( rest_url() ),
 'nonce' => wp_create_nonce( 'wp_rest' )
) );

Then in your JavaScript, set the header on every request:

xhr.setRequestHeader( 'X-WP-Nonce', wpApiSettings.nonce );

Where this fix does NOT apply

This fix does not apply if you are already sending credentials and the server is stripping them. If your curl command includes -u and you still get 401, the header is not reaching PHP. Move to Cause 2. This fix also does not apply to cross-origin requests using cookie auth: cookies and nonces do not work across domains, and you need application passwords instead.

Cause 2: Server strips the Authorization header

MacBook with dark code editor and terminal open showing server configuration

This is the single most common cause of 401 errors on application-password integrations. Apache running PHP via mod_cgi or PHP-FPM strips the Authorization header before it reaches PHP by default. The request arrives at WordPress with no credentials, WordPress treats it as anonymous, and you get 401 even though your client is sending a valid Authorization: Basic header.

The signature symptom: GET requests succeed because public reads need no auth. POST requests fail with 401 because writes require auth, and the credentials never arrive. If you inspect $_SERVER['HTTP_AUTHORIZATION'] inside WordPress, it will be empty or absent.

How to confirm it

Add this temporary check to your theme’s functions.php or a must-use plugin, then make an authenticated request:

add_action( 'rest_api_init', function() {
 error_log( 'AUTH HEADER: ' . var_export(
 $_SERVER['HTTP_AUTHORIZATION'] ?? $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ?? 'MISSING',
 true
 ) );
}, 1 );

Check wp-content/debug.log after the request. If it logs MISSING, the header is being stripped. If it logs the base64 string, the header is arriving and the problem is elsewhere.

The fix for Apache (.htaccess)

Add these lines to your .htaccess file, above the standard WordPress rewrite rules. Order matters: the [L] flag on later rules would prevent this one from executing if placed after them:

SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1
RewriteEngine On
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]

On Apache 2.4.13 or later, you can use the cleaner directive in your server config or virtual host block (not .htaccess):

CGIPassAuth On

The fix for Nginx

Nginx does not always pass the Authorization header to PHP-FPM by default. Add this line inside the fastcgi section of your server block, typically in the location ~ .php$ block:

fastcgi_pass_header Authorization;

Then reload Nginx: sudo nginx -s reload. If you are on a managed host that does not give you root access, contact support and ask them to enable passing of the Authorization header.

HTTPS requirement for application passwords

Even with the header passing correctly, application passwords will not work over plain HTTP. WordPress checks is_ssl() and silently disables the feature. To confirm, send a GET request to your site’s REST root:

curl -i https://example.com/wp-json/

Look at the authentication key in the JSON response. If it contains an application-passwords object, the feature is available. If the key is empty or missing, application passwords are disabled, usually because the site is not on HTTPS or is_ssl() returns false behind a reverse proxy.

The is_ssl() false-positive trap: your site loads over HTTPS in the browser, the padlock is there, and WordPress still refuses to enable application passwords. This happens when TLS terminates at a reverse proxy, load balancer, or Cloudflare, which forwards to your origin over plain HTTP. is_ssl() reads $_SERVER['HTTPS'], which the proxy never sets. Fix it in wp-config.php, above the require_once ABSPATH . 'wp-settings.php'; line:

if ( isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] )
 && 'https' === $_SERVER['HTTP_X_FORWARDED_PROTO'] ) {
 $_SERVER['HTTPS'] = 'on';
}

Only trust that header when a proxy you control sets it. If clients can reach your origin directly, an attacker can forge X-Forwarded-Proto: https over plain HTTP and re-enable application passwords on an unencrypted connection. On Cloudflare Flexible SSL, the origin hop really is unencrypted. Switch to Full (strict) instead of masking the warning.

Where this fix does NOT apply

This fix does not apply to cookie auth. Cookie auth does not use the Authorization header. If your requests come from a logged-in browser session and you are seeing 401, the issue is the nonce, not the header. Move to Cause 3. This fix also does not apply if your hosting provider strips the header at the platform level, above what .htaccess or Nginx config can control. Contact your host directly in that case.

Cause 3: Missing or expired nonce in cookie auth

This cause only affects cookie authentication, not application-password auth. If you are using application passwords, skip this section entirely. The nonce system exists to prevent CSRF: without a valid nonce, WordPress treats the request as anonymous even if the user is logged in and their session cookies are present.

The function responsible is rest_cookie_check_errors() in wp-includes/rest-api.php. It runs as a filter on rest_authentication_errors and handles three cases, per the WordPress core reference:

  • No nonce at all: WordPress calls wp_set_current_user( 0 ), making the request anonymous. The user is logged in, their cookies are present, but without a nonce the request is treated as unauthenticated. You get 401 on authenticated endpoints.
  • Invalid or expired nonce: WordPress returns WP_Error( 'rest_cookie_invalid_nonce', 'Cookie check failed', array( 'status' => 403 ) ). Note the status: this is 403, not 401.
  • Valid nonce: WordPress sends a refreshed nonce back in the X-WP-Nonce response header and returns true.

How to confirm it

Open your browser’s DevTools, go to the Network tab, and inspect the failing request. Look for the X-WP-Nonce header in the request headers. If it is absent, you are not sending a nonce at all. If it is present but the response contains rest_cookie_invalid_nonce, the nonce has expired.

A common variation: the request works in XMLHttpRequest but fails in fetch(). This happens because fetch() does not send cookies by default. Without cookies, the session is absent, and even a valid nonce cannot authenticate the request. The fix is to include credentials: 'same-origin' in your fetch options, as documented in the REST API Handbook:

fetch( endpoint, {
 method: 'POST',
 credentials: 'same-origin',
 headers: {
 'Content-Type': 'application/json',
 'X-WP-Nonce': wpApiSettings.nonce
 },
 body: JSON.stringify( data )
} );

Nonce lifecycle: why it expires

WordPress nonces are not truly “number used once.” They are valid for a time window, not for a single request. The default lifetime is controlled by wp_nonce_tick(), which uses the nonce_life filter with a default of DAY_IN_SECONDS (86,400 seconds). WordPress divides this into two 12-hour “ticks.” A nonce is valid for the current tick and the previous tick, so its actual lifetime ranges from 12 to 24 hours depending on when it was created within the tick cycle, per the WordPress Nonces documentation.

wp_verify_nonce() returns 1 if the nonce was generated in the current tick (first 12 hours), 2 if it was generated in the previous tick (12 to 24 hours ago), or false if it is invalid. When it returns 2, the nonce is in its second tick and will expire soon. WordPress uses this signal to send a refreshed nonce in the X-WP-Nonce response header on successful cookie-authenticated requests, so clients can update their stored nonce before it expires.

The fix for missing nonce

Ensure every request includes the nonce. The cleanest approach is to use the built-in wp.api JavaScript client, which handles nonce injection automatically via wp.api.models.Base. If you are making manual requests, generate the nonce in PHP and pass it to JavaScript:

wp_localize_script( 'wp-api', 'wpApiSettings', array(
 'root' => esc_url_raw( rest_url() ),
 'nonce' => wp_create_nonce( 'wp_rest' )
) );

Then set the X-WP-Nonce header on every request. The header name is case-sensitive: X-WP-Nonce, not X-WP-Header or x-wp-nonce. A wrong header name is a common mistake that produces a silent 401.

The fix for expired nonce

Read the refreshed nonce from the response header and update your stored value. On every successful REST response, WordPress sends a new X-WP-Nonce header. Your client should capture it and use it for subsequent requests. Gutenberg’s @wordpress/api-fetch package does this automatically: it catches rest_cookie_invalid_nonce errors, fetches a fresh nonce from the nonce endpoint, and retries the request. If you are building a custom client, you need to implement this refresh logic yourself.

The nonce refresh endpoint is available via the WordPress Heartbeat API or by calling wp_ajax_rest_nonce(), which returns a fresh nonce as plain text. You can also simply reload the page, which regenerates the nonce via wp_localize_script.

Where this fix does NOT apply

This entire section applies only to cookie auth. Application passwords do not use nonces. If you are authenticating with Authorization: Basic and getting 401, nonces are irrelevant. Also, this fix does not apply to cross-origin browser requests. Cookie auth requires same-origin context. If your JavaScript runs on a different domain than WordPress, cookies will not be sent, and you need application passwords or a custom auth method instead.

Cause 4: The rest_authentication_errors filter rejects requests

A rest_authentication_errors filter in your theme, a plugin, or a must-use plugin can reject REST API requests before they reach any endpoint. This filter is the official WordPress mechanism for requiring authentication on REST API requests. If it returns a WP_Error, the request fails. This affects both cookie auth and application-password auth.

The critical bug: if the filter callback checks is_user_logged_in() without first checking whether authentication already succeeded, it will reject application password requests. Application passwords authenticate via Basic Auth headers, not cookies, so is_user_logged_in() returns false for them. The filter then returns a WP_Error and the request fails with 401.

How to confirm it

Search your codebase:

grep -r "rest_authentication_errors" wp-content/

Check your active theme’s functions.php, any mu-plugins in wp-content/mu-plugins/, and active plugin files. Look for add_filter( 'rest_authentication_errors', ... ) callbacks. If all REST requests fail with 401, including public GET requests that should work without auth, this filter is likely the cause.

The fix

The filter callback must pass through any existing authentication result before applying its own check. Here is the correct pattern from the WordPress REST API Handbook:

add_filter( 'rest_authentication_errors', function( $result ) {
 // If a previous authentication check succeeded, pass it through.
 if ( true === $result || is_wp_error( $result ) ) {
 return $result;
 }

 // No authentication method has run yet.
 if ( ! is_user_logged_in() ) {
 return new WP_Error(
 'rest_not_logged_in',
 __( 'You are not currently logged in.' ),
 array( 'status' => 401 )
 );
 }

 return $result;
} );

The key detail is the first if check. If application passwords already authenticated the user, $result will be true and the filter returns early. Without that guard, the is_user_logged_in() check fails for application password requests because they authenticate via headers, not cookies.

If you cannot modify the filter because it lives in a plugin you do not control, override it with a higher-priority callback in a must-use plugin:

// Remove the restrictive filter.
remove_filter( 'rest_authentication_errors', 'problematic_callback_name' );

// Add the corrected version.
add_filter( 'rest_authentication_errors', function( $result ) {
 if ( true === $result || is_wp_error( $result ) ) {
 return $result;
 }
 if ( ! is_user_logged_in() ) {
 return new WP_Error(
 'rest_not_logged_in',
 __( 'You are not currently logged in.' ),
 array( 'status' => 401 )
 );
 }
 return $result;
} );

You need the exact callback function name to remove it. Search the plugin’s source for add_filter( 'rest_authentication_errors' to find the registered function name.

Where this fix does NOT apply

This fix does not apply if no rest_authentication_errors filter exists in your codebase. If grep returns nothing, the filter is not your problem. Also, this fix does not address security plugins that block REST routes through their own mechanisms rather than through the WordPress filter system. Those are covered in Cause 5. For a broader look at REST API blocking issues, see our guide on WordPress REST API disabled causes.

Cause 5: Security plugins blocking REST routes

Padlock on a glowing green and red keyboard representing web application firewall security blocking REST API requests

Security plugins with Web Application Firewall (WAF) features can block REST API requests before they reach WordPress core. The WAF sees the POST body containing HTML or JSON, flags it as suspicious, and returns 401 or 403 before the request even reaches the authentication layer. This affects both cookie auth and application-password auth, because the block happens upstream of WordPress entirely.

How to confirm it

Temporarily deactivate every security plugin on your site and retest the request. If it succeeds, reactivate plugins one by one, retesting after each activation, to identify which plugin is blocking. The most common offenders are Wordfence, Solid Security (formerly iThemes Security), and All In One WP Security.

Wordfence

Wordfence can block REST API requests through two mechanisms. First, the WAF flags POST or PUT requests with JSON or HTML payloads as suspicious and returns 403. Second, rate limiting blocks IPs making rapid sequential API calls, which is exactly what automation tools do.

For targeted diagnosis, check Wordfence > Tools > Live Traffic for blocked requests matching your API call. The block entry shows the reason and the triggered rule. You can also switch the WAF to Learning Mode temporarily (Wordfence > Manage WAF > Web Application Firewall Status > Learning Mode), make your API calls, then switch back to Enabled and Protecting. This trains the WAF to recognize your legitimate request pattern.

The fix: add the IP address of your automation tool to the allowlist under Wordfence > All Options > Advanced Firewall Options > Allowlisted IP addresses. For rate limiting, check Wordfence > All Options > Rate Limiting and disable or adjust thresholds for REST API paths.

Solid Security and All In One WP Security

Solid Security can restrict REST API access under Security > Settings > WordPress Tweaks. If “Restrict REST API access” is enabled, unauthenticated requests are blocked. All In One WP Security offers a similar restriction that blocks requests from “non-logged-in users.” The problem: “logged in” refers to cookie-based authentication, not Basic Auth via application passwords. Your application password request is treated as a non-logged-in user and gets blocked.

The fix: set REST API access to “Open” in Solid Security, or disable the REST API restriction in All In One WP Security. If you need to restrict access for security, restrict specific namespaces rather than blocking the entire API, and ensure the restriction respects application password authentication.

Host-level WAF and Cloudflare

If you have ruled out plugins and the request still fails, the block may be at the hosting or CDN layer. Cloudflare’s Bot Fight Mode and managed WAF rules can flag automated API requests as bot traffic and return 403. ModSecurity, enabled by default on many hosts, runs the OWASP Core Rule Set and can flag REST API POST requests with JSON payloads.

To confirm Cloudflare: temporarily set the DNS record to “DNS only” (gray cloud) and retest. If the request succeeds, the WAF is the cause. Create a custom firewall rule in Security > WAF > Custom Rules with the expression (http.request.uri.path contains "/wp-json/") and set the action to Skip. For tighter control, add an IP condition: (http.request.uri.path contains "/wp-json/") and (ip.src eq 203.0.113.50).

For ModSecurity: check the server error log for rule IDs:

sudo tail -100 /var/log/apache2/error.log | grep -i modsec

Find the rule ID (it looks like [id "949110"]) and disable that specific rule in your ModSecurity configuration:

SecRuleRemoveById 949110

Where this fix does NOT apply

This fix does not apply if no security plugins are installed and no CDN or host-level WAF is in front of your site. If you have confirmed that the request fails with no plugins active and no Cloudflare proxy, the block is not security-plugin-related. Also, these fixes do not address authentication failures where the credentials genuinely never reach WordPress due to server header stripping. That is Cause 2.

Cause 6: User role lacks required capabilities

This cause produces 403, not 401. It is included here because developers often encounter it during the same debugging session and the error message (“Sorry, you are not allowed to create posts as this user”) sounds like an authentication failure. It is not. The credentials were accepted. The user is authenticated. They simply lack the capability for the requested action.

WordPress checks create_posts (which maps to edit_posts by default) when creating a post via REST. If you specify "status": "publish", it also checks publish_posts. If the user lacks edit_posts, the request fails with rest_cannot_create before it reaches the publish check.

Roleedit_postspublish_postsCan create via REST?Can publish via REST?
SubscriberNoNoNoNo
ContributorYesNoYes (draft only)No
AuthorYesYesYesYes (own posts)
EditorYesYesYesYes
AdministratorYesYesYesYes

How to confirm it

Authenticate successfully and hit /wp-json/wp/v2/users/me?context=edit. The response includes the user’s roles. Cross-reference with the table above. Then test with "status": "draft" instead of "status": "publish". If the draft request succeeds but the publish request fails, the user has edit_posts but not publish_posts.

The fix

Either upgrade the user’s role to Author or above, or add specific capabilities programmatically:

$user = get_user_by( 'login', 'your_username' );
$user->add_cap( 'edit_posts' );
$user->add_cap( 'publish_posts' );

For automation workflows, a dedicated user with the Author role is the safest minimum. Editor gives full post management without admin-level access. For a complete walkthrough of capability-related errors, see our rest_cannot_create fix guide.

Where this fix does NOT apply

This fix does not apply to 401 errors. If you are getting 401, the user was never authenticated in the first place, and changing their role will not help. Confirm authentication works first by hitting /wp-json/wp/v2/users/me. Only proceed to capability checks if that endpoint returns the correct user.

How do I confirm the 401 error is fixed?

Source code showing Authentication Failed error message, representing the WordPress REST API 401 confirmation testing process

Run this curl sequence. It isolates each layer and tells you exactly where the pipeline works and where it breaks. Replace example.com, your_username, and your_app_password with your actual values.

Step 1: Verify the REST API is reachable

curl -i https://example.com/wp-json/

Expect HTTP 200 with a JSON response listing available namespaces. Check the authentication key for application-passwords availability. If this returns 404, the REST API is not routing. If it returns 403, something is actively blocking access.

Step 2: Verify authentication works

curl -i -u "your_username:your_app_password" https://example.com/wp-json/wp/v2/users/me

Expect HTTP 200 with the user object containing the correct ID. If this returns 401, authentication is failing. Check for header stripping (Cause 2) or the rest_authentication_errors filter (Cause 4). If it returns 403, check capabilities (Cause 6).

Step 3: Test a write operation

curl -i -u "your_username:your_app_password" 
 -X POST 
 -H "Content-Type: application/json" 
 -d '{"title":"Test Post","status":"draft"}' 
 https://example.com/wp-json/wp/v2/posts

Expect HTTP 201 Created with the post object. If this succeeds, the full pipeline works: routing, authentication, and capabilities. If it fails with 401, revisit Cause 2 or Cause 4. If it fails with 403, check the user role.

Step 4: Test with explicit Authorization header

If Step 2 failed, test with an explicit header to rule out curl’s -u flag:

curl -i -H "Authorization: Basic $(echo -n 'your_username:your_app_password' | base64)" 
 https://example.com/wp-json/wp/v2/users/me

If this also returns 401, the Authorization header is not reaching PHP. Go to Cause 2.

What a successful response looks like

HTTP/2 200
Content-Type: application/json

{
 "id": 1,
 "name": "admin",
 "slug": "admin",
 "capabilities": { ... }
}

Where do these fixes NOT apply?

The fixes in this guide assume you control the WordPress installation and its server configuration. They do not apply in these situations:

  • Managed hosts that strip headers at the platform level: Some managed WordPress hosts intercept requests before they reach your .htaccess or Nginx config. If you have applied the header-passing fixes and $_SERVER['HTTP_AUTHORIZATION'] is still missing, contact your host. Ask specifically: “Does your platform pass the Authorization header to PHP?”
  • Headless WordPress with a custom auth plugin: If you are using JWT, OAuth 2.0, or a custom authentication plugin instead of application passwords or cookie auth, the diagnostic path in this guide does not apply. Check the plugin’s documentation for its specific error handling.
  • Multi-site networks with REST API restrictions: WordPress multi-site can have network-level REST API restrictions that behave differently from single-site. The rest_authentication_errors filter may be applied at the network level. Check wp-content/mu-plugins/ on the network admin side.
  • Requests through a CDN that rewrites headers: Some CDN configurations strip or rewrite the Authorization header. Cloudflare in particular can interfere with Basic Auth if custom rules are misconfigured. Test with the CDN disabled (gray cloud) to isolate.

If you are automating WordPress content publishing and hitting these authentication errors repeatedly across multiple sites, ClearPost handles the authentication and publishing pipeline internally. The plugin uses WordPress’s native application password system and manages the full post creation workflow. You approve every post before it goes live. For everything else, the diagnostic steps above will resolve the 401 in the vast majority of cases.

Try ClearPost free for 7 days. AI does the heavy lifting, you approve every post before it goes live. No long onboarding, no agency overhead, cancel anytime.

Frequently Asked Questions

What does a WordPress REST API 401 error mean?

A 401 Unauthorized response means WordPress did not recognize the request as coming from an authenticated user. The request was treated as anonymous (user ID 0), and the endpoint required authentication. The most common cause is the server stripping the Authorization header before it reaches PHP, which affects application-password authentication. For cookie auth, the most common cause is a missing or expired nonce.

How do cookie auth and application-password auth differ for REST API 401 errors?

Cookie auth requires session cookies plus an X-WP-Nonce header (created with wp_create_nonce(‘wp_rest’)) and only works same-origin from within WordPress. Application passwords use an Authorization: Basic header, require HTTPS, and work from any origin. Cookie auth fails with 401 when the nonce is missing, and returns 403 with rest_cookie_invalid_nonce when the nonce is expired. Application passwords fail with 401 when the Authorization header is stripped by the server or HTTPS is not configured.

What causes rest_cookie_invalid_nonce and how do I fix it?

The rest_cookie_invalid_nonce error (HTTP 403) occurs when the nonce sent with a cookie-authenticated REST API request is invalid or expired. WordPress nonces have a lifetime of 12 to 24 hours. To fix it, ensure every request includes the X-WP-Nonce header generated by wp_create_nonce(‘wp_rest’). For fetch() requests, include credentials: ‘same-origin’ so session cookies are sent. Read the refreshed nonce from the X-WP-Nonce response header on each successful response and update your stored value. This error only affects cookie auth, not application-password auth.

Why does GET work but POST returns 401 in the WordPress REST API?

GET requests to public endpoints like /wp-json/wp/v2/posts do not require authentication, so they succeed even when credentials are missing. POST, PUT, and DELETE require authentication. If the server strips the Authorization header before it reaches PHP, GET requests work fine but POST requests fail with 401 because the credentials never arrive. The fix is to add SetEnvIf Authorization and RewriteRule directives to .htaccess (Apache) or fastcgi_pass_header Authorization to Nginx config to pass the header through to PHP.

How do I confirm the WordPress REST API 401 error is fixed?

Run curl -i -u “username:app_password” against /wp-json/wp/v2/users/me. If it returns HTTP 200 with the user object, authentication works. Then test a POST to /wp-json/wp/v2/posts with a draft status. If you get HTTP 201 Created, the full pipeline is working. If the users/me request still returns 401, test with an explicit Authorization header to rule out curl’s -u flag. If that also fails, the header is not reaching PHP and the server configuration needs fixing.