Every WordPress REST API integration starts with the same decision: how do external systems prove who they are? Get it right and you never think about it again. Get it wrong and you either lock yourself out of your own data or leave a door open to everyone else’s.
We build API integrations for client sites constantly โ CRM syncs, order pipelines, content automation. Three authentication methods cover almost every real case: Application Passwords, JWT, and OAuth. This guide explains how each works, shows working code, and tells you which one we reach for and when.
One thing first. If your integration runs inside WordPress itself โ a theme calling the API from the front end โ you do not need any of these. Cookie authentication with a nonce is already there. These three methods exist for external systems: scripts, servers, and services that live outside your install.
Application Passwords: the built-in default
WordPress has shipped Application Passwords in core since version 5.6. No plugin. A user generates a 24-character password scoped to API use, and external systems authenticate with standard HTTP Basic Auth.
Setting one up
Users โ Profile โ Application Passwords. Name the password after the system that will use it โ “brevo-sync”, not “test” โ generate, and copy it once. WordPress never shows it again.
Using it is one line of curl:
curl -u "username:xxxx xxxx xxxx xxxx xxxx xxxx" \
https://example.com/wp-json/wp/v2/posts?status=draft
Or from PHP:
$response = wp_remote_get( 'https://example.com/wp-json/wp/v2/posts', array(
'headers' => array(
'Authorization' => 'Basic ' . base64_encode( 'username:xxxx xxxx xxxx xxxx xxxx xxxx' ),
),
) );
What makes it good
- Zero dependencies. Core feature, maintained by core, no plugin to abandon you.
- Per-integration revocation. Each system gets its own password. When you retire a service, you revoke one credential and nothing else breaks. The profile screen shows when each password was last used โ quiet gold for audits.
- It inherits user permissions. A password for an Editor-role user cannot manage plugins. Create a dedicated user with the minimum role per integration and you have least-privilege access without writing a line of code.
The catches
- HTTPS is mandatory. Basic Auth sends the credential with every request. Over plain HTTP it is readable in transit โ WordPress refuses Application Passwords on non-HTTPS sites for exactly this reason.
- The credential is long-lived. No expiry, no rotation built in. If it leaks, it works until someone revokes it.
- Some hosts strip the Authorization header. If auth fails mysteriously, check whether your host forwards it. On Apache, this line in .htaccess usually fixes it:
SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1
JWT: tokens for stateless systems
JSON Web Tokens flip the model. Instead of sending a credential with every request, the client exchanges a username and password once for a signed token, then presents the token. Tokens expire โ typically after an hour โ and the server validates them by checking the signature rather than hitting the database for a session.
WordPress core does not ship JWT support. Adding it means either implementing token issuance yourself against the JWT specification or installing an authentication layer โ and whichever route you take, you are trusting that code with your login credentials, so read it before you rely on it. The flow looks the same either way:
# Exchange credentials for a token at your token endpoint.
curl -X POST https://example.com/wp-json/your-auth/v1/token \
-d "username=api-user&password=secret"
# Use the token until it expires.
curl https://example.com/wp-json/wp/v2/posts \
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOi..."
When JWT earns its place
- Mobile apps and SPAs. Short-lived tokens suit clients you do not fully control. A leaked token expires on its own; a leaked Application Password does not.
- High-volume machine-to-machine traffic. Signature validation is cheap and stateless, which matters at thousands of requests per minute.
- Multi-service architectures. One token can be validated by several services sharing the signing secret, without each one calling back to WordPress.
The catches
- You own the security layer. Whether you implement it or install it, JWT is code outside WordPress core protecting your front door. Audit it and keep it current.
- Token refresh is your problem. Expiry is a feature, but every client needs re-authentication logic. More moving parts, more failure modes.
- The signing secret is the kingdom. It lives in wp-config.php. Anyone who obtains it can mint valid tokens for any user. Treat it like a root password and rotate it if staff with server access leave.
OAuth 2.0: for when other people’s users log in
OAuth solves a different problem than the other two. Application Passwords and JWT authenticate your systems. OAuth lets third parties access your API on behalf of users who consent โ the “Sign in with Google” pattern, pointed at your WordPress install.
If you are building a platform where external developers request access to your users’ data, OAuth is the correct and standard answer. For everything else it is overkill: an authorisation server, consent screens, token lifecycle management, and client registration โ a lot of machinery when both ends of the integration are you.
In fourteen years of client work we have needed full OAuth on WordPress fewer than five times. When we did, nothing else would have been right. That is the shape of it: rarely needed, irreplaceable when it is.
Which one we deploy
After enough integrations, the decision compresses to four rules:
- Server-to-server sync โ CRM, ERP, fulfilment โ gets Application Passwords. Core-maintained, revocable per system, least privilege through user roles. This covers most of what businesses actually build.
- Mobile apps and JavaScript front ends get JWT. Short-lived tokens limit the damage of a leak on clients you do not control.
- Third-party developers accessing your users’ data get OAuth 2.0. Consent and scoped access are the whole point โ nothing else does this job.
- Theme code calling the API same-origin gets cookies and a nonce. It is already built in. Adding more machinery here is complexity without benefit.
Our default is Application Passwords, and the pattern we deploy with it every time: one dedicated WordPress user per integration, minimum role, one Application Password each. The accounting sync gets a user that can read orders and nothing else. When it misbehaves, its last-used timestamp and its own credential make the audit trail obvious.
Hardening whatever you choose
Method-independent rules we apply on every build:
- HTTPS everywhere, no exceptions. Every method here sends secrets over the wire.
- Least privilege by user design. Do not hand an integration an administrator credential because it was quicker. It is always quicker, right up until it is very much not.
- Write a real permission_callback. If you register custom endpoints, the callback decides who gets in. Returning
trueis publishing your endpoint to the internet:
register_rest_route( 'wprobo/v1', '/orders-summary', array(
'methods' => 'GET',
'callback' => 'wprobo_orders_summary',
'permission_callback' => function() {
return current_user_can( 'view_woocommerce_reports' );
},
) );
- Rate-limit at the edge. Auth failures should get expensive quickly. Your host, Cloudflare, or a security plugin can all do this โ pick one and turn it on.
- Audit credentials quarterly. The Application Passwords screen shows last-used dates. A credential unused for six months should not exist.
Where to go from here
Authentication is the front door, but the integration behind it is where the value lives. We covered the broader patterns โ webhooks versus polling, error handling, sync design โ in our WordPress API integrations guide, with a worked example in the Salesforce integration walkthrough.
And if you have an integration that needs building โ or one that was built badly and needs rescuing โ that is day-job territory for us. Tell us what you are connecting via our services page and we will tell you honestly whether it is a two-day job or a two-week one.
