LEARN · UPDATED 2026-09-21

Why the WordPress REST API reads but will not write

401 and 403 on write requests almost always mean one of four things. Here is the test that tells them apart in one request, and the fix for each.

The symptom is oddly specific and very common. GET /wp-json/wp/v2/posts returns your posts exactly as expected. The same credential, sent to the same site, on a POST to create one, comes back 401 or 403. Nothing in the message says what is wrong, and the obvious conclusion — the credential is wrong — is usually the one that is not true.

There are four causes. Three of them are on the WordPress side, one is in how the connection was set up, and they produce almost identical output. This page is the order to test them in.

First, read the code, not the message

WordPress returns a machine-readable code alongside the human message, and the code is the part worth reading. The message is written for a person and is deliberately vague; the code says which layer refused.

HTTPcodeWhat it actually means
404rest_no_routeThe route does not exist. Wrong path, or the post type has no show_in_rest
401rest_not_logged_inWordPress saw no authenticated user for a request that needs one
403rest_forbiddenThere is a user, and that user lacks the capability
403rest_cannot_create, rest_cannot_edit, rest_cannot_deleteSame thing, named per operation
401an error about the password itselfThe credential arrived and was rejected

The split that matters is the last row against the two above it. An error about the credential proves the credential reached PHP. An error about being not logged in or not allowed proves the opposite is worth checking: perhaps nothing arrived at all.

The one test that separates the two big causes

Send a request with a deliberately wrong application password.

  • If WordPress complains that the password is invalid — it received an Authorization header, read it, and rejected the value. The header is getting through. Your problem is permissions, and you can skip to the role section below.
  • If WordPress answers as though no credential was sent at all — reads succeed, writes come back rest_not_logged_in or rest_cannot_create — then the header never reached PHP. It was stripped in transit.

This is worth doing before anything else because the two causes need completely different fixes and produce the same user-visible behaviour: everything reads, nothing writes.

WordPress ships the same check in Site Health. Tools → Site Health → Info, under the server section, reports whether the Authorization header is being received. It is a faster version of the same test when you have wp-admin access.

Cause one: the header is stripped before PHP sees it

This is the diagnosis people miss, and it is not a WordPress bug. Authorization is a header some server configurations drop on purpose, and when it is dropped WordPress cannot know it ever existed. Every request then arrives as an anonymous visitor. Public reads work, because public reads are public. Writes are refused, because anonymous visitors cannot write.

Where it happens, and what fixes it:

Apache with mod_rewrite. Put this in .htaccess above the WordPress block:

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

Apache with CGI or FastCGI. Apache 2.4.13 and newer has a directive for exactly this:

CGIPassAuth On

nginx with PHP-FPM. The header has to be passed explicitly in the location block that hands requests to PHP:

fastcgi_param HTTP_AUTHORIZATION $http_authorization;

On managed hosting you generally cannot edit any of these, and the answer is a support ticket that says "the Authorization header is not reaching PHP on my site". Hosts recognise that sentence. They do not recognise "the REST API is broken".

Cause two: a security plugin or host rule is refusing REST requests

Before blaming authentication, confirm the API answers at all. Open https://example.com/wp-json/ in a browser. You should see JSON describing the site's routes. A 404, a login page, or an HTML error page means something is refusing REST requests outright.

The usual culprit is a security plugin with a "disable the REST API" or "require authentication for the REST API" switch, and the usual fix is to allow requests that carry an Authorization header rather than disabling the protection entirely. Some firewalls also block POST, PUT and DELETE to /wp-json/ while allowing GET, which reproduces the reads-but-never-writes symptom precisely.

One more prerequisite lives here: application passwords require HTTPS. WordPress refuses to issue them over plain HTTP unless WP_ENVIRONMENT_TYPE is set to local. If the certificate is broken or the site is mixed-content, fix that first — the credential screen will not even appear otherwise.

Cause three: the role is too small for the capability

If the wrong-password test proved the header arrives, the refusal is a real one. WordPress checked a capability and the account did not have it. The good news is that this error names itself: rest_cannot_edit on a template is a different sentence from rest_cannot_create on a post, and the operation named is the capability to go looking for.

The mapping that catches most people:

What you are changingCapabilitySmallest role that has it
Posts, pages, custom post typesedit_posts, publish_postsAuthor or Editor
Other people's postsedit_others_postsEditor
Media uploadsupload_filesAuthor
Categories and tagsmanage_categoriesEditor
Comment moderationmoderate_commentsEditor
Templates, template parts, global stylesedit_theme_optionsAdministrator
Menus, navigation, widgetsedit_theme_optionsAdministrator
Site title, tagline, front pagemanage_optionsAdministrator

The last three rows are the ones that surprise people. Everything to do with how a block theme looks sits behind edit_theme_options, which is an administrator capability. An Editor account can write every post on the site and still be refused a change to a template part. There is no middle role in WordPress that covers block-theme design without also covering plugin installation — that is a real limitation of the capability model, not something a client or a connector can work around.

So: give the credential Editor when the work is content, and Administrator only when design is genuinely in scope.

Cause four: the connection itself is read-only

The last one is not a WordPress problem at all. If whatever is holding the credential was configured read-only, no write will ever leave it, and the error you see may be generated before a request is made.

A well-built client makes this obvious rather than subtle: it registers no write tools at all, so there is nothing to call, instead of offering tools that fail at the far end. If you are using an MCP connector, ask the assistant for site information — it should report the read-only state plainly. wpreef's wp_site_info is built for this: it reports the WordPress version, the credential's capabilities, and every tool unavailable on that site together with the step that would enable it.

The order to work through

  1. Open /wp-json/ in a browser. JSON means the API is reachable; anything else is cause two.

  2. Confirm the site is on HTTPS, or application passwords will not exist to test with.

  3. Send one request with a deliberately wrong password. An error about the password means the header arrives — go to step 5. Behaviour as though no credential was sent means the header is stripped — that is cause one.

  4. Fix the header at the web server, or ask the host to.

  5. Read the code on the refusal, find the capability it names, and check the account's role.

  6. If everything above is clean, check whether the connection was built read-only.

Four causes, one test that halves the search space, and an error code that names the capability once you are past it. Almost every "the WordPress REST API is broken" thread is one of these.

Questions people ask

Why does the WordPress REST API return 401 on POST but work on GET?

Because public reads do not need a user and writes do. A GET that succeeds proves only that the route exists, not that your credential arrived. The usual cause is the Authorization header being stripped by Apache, FastCGI or nginx before PHP sees it, so every request reaches WordPress as an anonymous visitor.

What does rest_forbidden mean in WordPress?

It means WordPress authenticated a user and that user lacks the capability for the operation. It is a permissions refusal, not an authentication one — so the credential is fine and the role attached to it is too small. The per-operation variants rest_cannot_create, rest_cannot_edit and rest_cannot_delete mean the same thing and name the operation.

How do I know whether the Authorization header is reaching WordPress?

Send a request with a deliberately wrong application password. If WordPress says the password is invalid, the header arrived. If it behaves as though no credential was sent — reads work, writes return rest_not_logged_in — the header was stripped. WordPress also reports this under Tools → Site Health → Info in the server section.

Why can my Editor account not change a template or menu?

Templates, template parts, global styles, navigation menus and widgets are all gated on edit_theme_options, which only Administrator holds. WordPress has no role that grants block-theme design without also granting plugin installation, so design work over REST needs an administrator credential.

Do application passwords work over plain HTTP?

No. WordPress hides the application passwords screen entirely unless the site is on HTTPS, or WP_ENVIRONMENT_TYPE is set to local for development. If the option is missing from Users → Profile, check the certificate before anything else.

← All articles