What the API exposes without login
The REST API at /wp-json/wp/v2/ serves published posts, pages, media, categories, tags and public user data to anyone, unauthenticated. That is by design; it is the same data the front end shows. It also serves, unauthenticated, the list of users who have published posts (/wp-json/wp/v2/users), which reveals usernames and is worth restricting on sites that do not need it.
Anything beyond reading public content, drafts, private posts, creating or editing, custom endpoints that touch private data, needs authentication. There are five ways to do it and they suit different callers.
1. Cookie authentication with a nonce
For JavaScript running on the WordPress site itself, in a logged-in session. The browser sends the WordPress login cookie automatically; the request must also include a nonce in the X-WP-Nonce header, generated with wp_create_nonce('wp_rest') and passed to the script via wp_localize_script or the wpApiSettings global.
Use it for: the block editor, admin screens, front-end features for logged-in users on the same domain.
Do not use it for: anything calling from another domain or from a server. Cookies do not travel, and the nonce is tied to the session.
2. Application passwords
Built in since WordPress 5.6. A user generates a long random password from their profile, scoped to that user, revocable individually. The caller sends HTTP Basic auth with the username and the application password, over HTTPS only.
Authorization: Basic base64(username:xxxx xxxx xxxx xxxx xxxx xxxx)Use it for: server-to-server integrations (a Next.js build fetching drafts, a CRM syncing contacts, a script publishing posts), where the caller can keep a secret. Create a dedicated user with the minimum role for the task, not an administrator, and one application password per integration so each can be revoked alone.
Do not use it for: browser-side code, where the password would be visible.
Application passwords are disabled by default on sites not served over HTTPS and can be disabled entirely with wp_is_application_passwords_available if a site does not need them.
3. JWT (JSON Web Tokens)
A plugin (JWT Authentication for WP REST API, or WPGraphQL's JWT extension) issues a signed token in exchange for username and password, and the caller sends it as a bearer token on subsequent requests. Tokens expire and can be refreshed.
Use it for: headless front ends where users log in through the React app and the app calls WordPress on their behalf; mobile apps.
Cautions: the signing secret must be strong and in wp-config.php; tokens must be stored securely on the client (memory or httpOnly cookie, not localStorage if the front end has any XSS surface); short expiry with refresh; the login endpoint rate-limited.
4. OAuth 2
For third parties acting on a user's behalf with the user's consent, the way "Log in with Google" works. Plugins exist (WP OAuth Server and others). Heavier to set up.
Use it for: a public API where other applications ask your users for permission. Rare for business sites.
5. Custom API keys for custom endpoints
For your own endpoints registered with register_rest_route, called by your own systems, a simple shared key in a header checked by the permission_callback is adequate and common:
register_rest_route( 'acme/v1', '/sync', [
'methods' => 'POST',
'callback' => 'acme_handle_sync',
'permission_callback' => function ( WP_REST_Request $req ) {
$key = $req->get_header( 'x-acme-key' );
return $key && hash_equals( ACME_SYNC_KEY, $key );
},
] );The key is a constant in wp-config.php, long and random, rotated when staff change. Use hash_equals to avoid timing attacks. Log calls.
Use it for: webhooks from your own services, the revalidation call from a headless front end, an import trigger.
The mistakes that expose data
permission_callback returning true. WordPress 5.5 made the callback mandatory and a lot of code satisfied the requirement with '__return_true'. That endpoint is public. Every custom endpoint needs a real check: a capability test, a key comparison, or an explicit statement that the data is public.
Trusting the request body for identity. A user_id in the JSON is not authentication. The identity comes from get_current_user_id() after WordPress has authenticated the request, or from your key check.
Administrator credentials in an integration. The CRM sync runs as an admin because it was easiest. When the CRM is breached, so is the site. Dedicated users with the smallest role that works.
Secrets in the repository or the theme. Application passwords, JWT secrets and API keys live in wp-config.php or environment variables, never in committed code.
No rate limiting on login endpoints. JWT and application password authentication are password checks; without limits they are brute-force targets. A firewall rule or a plugin limiting attempts per IP.
Leaving the users endpoint public. Filter rest_endpoints to remove /wp/v2/users for unauthenticated requests on sites with no reason to expose authors.
HTTP. All of the above assume HTTPS. Credentials over HTTP are credentials in the clear.
Choosing quickly
| Caller | Method |
|---|---|
| JavaScript on the WordPress site, logged-in user | Cookie + nonce |
| Your own server or build process | Application password, dedicated user |
| Headless app where end users log in | JWT via plugin |
| Third-party apps acting for your users | OAuth 2 |
| Your own webhooks and triggers | Custom key in permission_callback |
Where this sits
The authentication choice is one of the first decisions in any headless or integrated WordPress build, and reviewing custom endpoints' permission callbacks is a standard item when we audit a site that has them, because __return_true is still the most common way a WordPress site leaks data it thought was private.