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.
| HTTP | code | What it actually means |
|---|---|---|
| 404 | rest_no_route | The route does not exist. Wrong path, or the post type has no show_in_rest |
| 401 | rest_not_logged_in | WordPress saw no authenticated user for a request that needs one |
| 403 | rest_forbidden | There is a user, and that user lacks the capability |
| 403 | rest_cannot_create, rest_cannot_edit, rest_cannot_delete | Same thing, named per operation |
| 401 | an error about the password itself | The 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
Authorizationheader, 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_inorrest_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 Onnginx 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 changing | Capability | Smallest role that has it |
|---|---|---|
| Posts, pages, custom post types | edit_posts, publish_posts | Author or Editor |
| Other people's posts | edit_others_posts | Editor |
| Media uploads | upload_files | Author |
| Categories and tags | manage_categories | Editor |
| Comment moderation | moderate_comments | Editor |
| Templates, template parts, global styles | edit_theme_options | Administrator |
| Menus, navigation, widgets | edit_theme_options | Administrator |
| Site title, tagline, front page | manage_options | Administrator |
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
Open
/wp-json/in a browser. JSON means the API is reachable; anything else is cause two.Confirm the site is on HTTPS, or application passwords will not exist to test with.
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.
Fix the header at the web server, or ask the host to.
Read the
codeon the refusal, find the capability it names, and check the account's role.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.