The WordPress REST API, and How Exposed Yours Is
The REST API is not an optional add-on you can switch off — the block editor and much of the admin depend on it. The real work is choosing an authentication model and deciding what anonymous callers are allowed to read.
Start from what depends on it. The WordPress REST API is not a plugin feature bolted on for developers — the block editor, the site editor and large parts of the modern admin talk to your site through it. Treating it as something to switch off is how people break their own editor and then blame an update.
The useful questions are narrower: what is it for, how do callers prove who they are, and how much can a stranger read without proving anything.
What it is actually for
The API exposes WordPress content and settings as JSON over HTTP, which makes three things possible that were painful before.
- Decoupled front ends. A separate application renders the site and uses WordPress purely as an editorial back end.
- Integrations. External systems create posts, read orders, sync users or trigger workflows without screen-scraping the admin.
- The admin itself. Core's own interfaces are API clients. That is the fact that changes how you should secure it.
Content types register themselves with the API, which means a plugin's custom post type or custom fields may become readable the moment it is activated — often without the plugin author thinking hard about who should see them.
Finding the interface
With pretty permalinks enabled, the base is /wp-json/, and core's own namespace is wp/v2:
https://example.com/wp-json/
https://example.com/wp-json/wp/v2/posts
The root route returns a description of the site and the namespaces available, which is the honest way to enumerate what a given install exposes rather than guessing route names.
When permalinks are set to plain, the same routes are reached through a query parameter instead:
https://example.com/?rest_route=/wp/v2/posts
If a request returns a 404 that looks like a WordPress page rather than a JSON error, the permalink structure is usually the reason — see permalinks not working before assuming the API is disabled.
WordPress also advertises the API in a Link header and a <link> element on the front end. That is how clients discover it automatically, and it is why "hiding" the API by removing that tag achieves nothing: the routes still answer. You can confirm what a site advertises with an HTTP header check.
Authentication models
There is no single blessed method, which is the part newcomers find unsatisfying. Pick by caller type.
| Caller | Model | Notes |
|---|---|---|
| Browser, same site, logged in | Cookies plus a nonce | What the block editor uses; the nonce must accompany write requests |
| A script or service you own | Application passwords | Per-user credentials, generated in the user profile, revocable individually |
| A third-party application | Token-based schemes via a plugin | OAuth or JWT style flows; not in core |
| Anonymous reader | None | Only public read routes answer |
Two principles hold across all of them.
Capabilities decide everything. Authentication only establishes who is calling; what happens next is the ordinary WordPress permission system. An API credential belonging to a Subscriber cannot publish, whatever the request says. Getting user roles right is API security, not a separate task.
Credentials that travel on every request need TLS without exception. Application passwords are sent with each call, so on plain HTTP they are readable in transit — which is why core will not issue them over an insecure connection.
Keep secrets out of the codebase. Constants in wp-config.php, or environment variables read by it, are the normal home for anything a deployment needs.
What an anonymous request can read
This is the part most sites never audit. By default, published content is public — that is correct and expected, since it is public on the site too.
The uncomfortable cases are these.
- User listings. Author accounts can be enumerated through the API, returning usernames and display names. That is not a breach, but it hands a brute-force attempt half its work.
- Plugin-registered data. Custom post types and meta fields exposed by a plugin, sometimes including material the site owner assumed was internal.
- Volume. Core applies no rate limiting, so one client can page through your entire content library as fast as the server answers.
Reducing exposure without breaking the editor
The principle is narrow restriction, not a blanket block.
- Require authentication for the user routes rather than for everything, so enumeration stops while the editor keeps working.
- Audit what plugins register. Read the root route on a staging copy after each significant plugin addition and look for anything you did not expect.
- Rate limit at the edge. A WAF or reverse proxy is the right layer for this; PHP is not.
- Use distinct credentials per integration, so one can be revoked without disturbing the others.
- Log and review. When something behaves oddly, debug mode will surface API errors that the JSON response summarises unhelpfully.
Sites with genuinely private content should serve it from a separate authenticated route, not a plugin that hides a public one.
Where to start on your own site
Open your root route in a browser and read the list of namespaces. If names appear that you cannot account for, you have found the work. Then check the user routes anonymously and decide whether that output is something you want to publish — because right now, you are publishing it.
Frequently asked
- No. The block editor, the site editor and several core admin screens are REST clients, so a blanket block breaks the admin experience. Restrict specific routes for unauthenticated callers instead of switching the whole interface off.
- Pretty permalinks give you the /wp-json/ prefix. When permalinks are set to plain, the same routes are reached through the rest_route query parameter instead. Both are normal; the discovery link in the page headers tells you which one applies.
- They are safe when the site is served strictly over HTTPS, because they travel as credentials on each request. They are per-user and individually revocable, but core does not scope them to particular capabilities, so a compromised one carries everything that user can do.