/wp-json/wp/v2/users answers anonymous requests on a default WordPress install. The reply carries an id and a slug for each author.
That slug is the same value WordPress puts in the author archive address, /author/<slug>/.
The REST API Handbook's FAQ is blunt about the obvious fix. It says you should not disable the REST API, because WordPress Admin functionality depends on it. So the work below is narrower: close one route, decide whether to require a login for the rest, then test both with curl.
See what your site answers today
Run these from a machine that isn't logged in to the site:
curl -s -o /dev/null -w "%{http_code}\n" https://example.com/wp-json/wp/v2/users
curl -s -o /dev/null -w "%{http_code}\n" "https://example.com/?rest_route=/wp/v2/users"
The second form matters. The Handbook says sites without pretty permalinks take the route as a rest_route query parameter, and that form also answers on sites that do have pretty permalinks. A block that only matches the /wp-json/ path misses it.
A 200 on either line means the list is public.
List the plugin namespaces
The API index has a namespaces item. Every plugin that calls register_rest_route() adds its own entry there:
curl -s https://example.com/wp-json/ | jq -r '.namespaces[]'
On a WooCommerce site the output includes wc/store/v1 next to wp/v2 and oembed/1.0.
Patchstack's 2026 report counted 11,334 vulnerabilities in the WordPress ecosystem in 2025. 91% of them were in plugins.
Remove the users routes for logged-out visitors
Put this in a small plugin (a file in wp-content/mu-plugins/ works):
<?php
add_filter( 'rest_endpoints', function ( $endpoints ) {
if ( ! is_user_logged_in() ) {
unset( $endpoints['/wp/v2/users'] );
unset( $endpoints['/wp/v2/users/(?P<id>[\d]+)'] );
}
return $endpoints;
} );
The filter runs inside WordPress after the route is parsed, so /wp-json/ and ?rest_route= both lose the route. Run the two curl lines again. Both should print 404 (the code WordPress returns as rest_no_route).
The block editor still loads the users list for logged-in editors, since the check only applies to anonymous requests.
Requiring a login for every route
The Handbook's own answer for anonymous access is the rest_authentication_errors filter with an is_user_logged_in() check:
<?php
add_filter( 'rest_authentication_errors', function ( $result ) {
if ( ! empty( $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;
} );
The first if keeps any earlier authentication result, including an error from another plugin.
WooCommerce's documentation calls the Store API "an unauthenticated API", and the cart and checkout blocks call it as the shopper. With the filter on, /wp-json/wc/store/v1/cart returns a 401 to every visitor.
Test these before shipping it to a live site:
| Check | Command or action | Expected with the login filter |
|---|---|---|
| Users route | curl -s -o /dev/null -w "%{http_code}\n" https://example.com/wp-json/wp/v2/users |
401 |
| Store API cart | curl -s -o /dev/null -w "%{http_code}\n" https://example.com/wp-json/wc/store/v1/cart |
401, so the cart block breaks |
| Embeds | Paste a post URL into another WordPress site's editor | No preview card |
| Contact form | Submit the form in a private window | Fails if the plugin posts to a REST route |
If any row on that list is a feature you sell with, stay with the users filter alone.
The discovery header still points at the API
The Handbook's discovery page says the REST API adds a Link header to every front-end page:
curl -sI https://example.com/ | grep -i 'api.w.org'
The header reads Link: <https://example.com/wp-json/>; rel="https://api.w.org/", and the same address sits in a <link rel='https://api.w.org/'> element in the page head. If you move the API to a custom base path, run the command again and note which address the header now gives out to every crawler.
Five-minute check
Run the two users-route curl lines against one production site you manage today, before and after the rest_endpoints filter, and keep the output.
Has anyone found a plugin that breaks when only the users routes are removed for logged-out visitors?
Top comments (0)