I ended up using WordPress Application Passwords while building a small publishing tool that talks to a WordPress site from outside the browser. The first version was simple: send a title, send some body content, create a draft through the REST API, then open WordPress later for editing. Sounds boring in the best way.
The part that made me slow down was auth. I did not want to ask users for their real WordPress password. I also did not want to build a full OAuth flow for a tiny maker tool. Application Passwords landed right in the middle: built into WordPress, easy to revoke, made for API access, and good enough for a lot of indie workflows if you treat them with care.
The mental model
An Application Password is not a second login password for the WordPress admin screen. It is a token-like password attached to a specific user account. When an external app sends API requests, WordPress checks the username plus that generated password and then runs the request as that user.
That last part matters. If the user is an Administrator, the API request has Administrator-level powers. If the user is an Author, it gets Author-level powers. Application Passwords do not add fine-grained scopes by default. They inherit the permissions of the user they belong to.
The simplest rule I kept repeating while building: make a dedicated WordPress user for the tool, give it only the role it needs, then create an Application Password for that user.
For my first test, I used my admin account because I wanted to get a request working fast. That was fine for five minutes of local testing. Then I backed up, created a separate user, gave it the Author role, and generated a fresh password. Small course correction, much better setup.
Creating the password in WordPress
The admin UI makes this part pretty painless. I went to the user profile screen, found the Application Passwords section, gave the password a label, and clicked the button to generate it. The label is just for humans. I named mine after the tool so I could recognize it later when it was time to rotate or revoke access.
- Open the WordPress dashboard.
- Go to Users, then open the profile for the user your tool will act as.
- Scroll to Application Passwords.
- Enter a clear app name, like “Local Draft Publisher” or “Build Notes Importer”.
- Generate the password and copy it immediately.
WordPress only shows the generated password once. That is not a bug. That is the whole point. I pasted it into a local environment file, not into my code. The first time I copied it, I accidentally grabbed a trailing space and spent a few minutes blaming my fetch request. Classic.
The request that finally worked
Application Password auth uses HTTP Basic Auth. That sounds old, but in this case WordPress is using it as a simple transport for the username and generated app password. The important part is that the request goes over HTTPS. Without HTTPS, Basic Auth is a bad idea because the credential can be exposed in transit.
The header format looks like this:
Authorization: Basic base64(username:application_password)
In Node, my first working request looked like this:
const username = process.env.WP_USERNAME;
const appPassword = process.env.WP_APP_PASSWORD;
const siteUrl = process.env.WP_SITE_URL;
const auth = Buffer
.from(`${username}:${appPassword}`)
.toString('base64');
const response = await fetch(`${siteUrl}/wp-json/wp/v2/posts`, {
method: 'POST',
headers: {
'Authorization': `Basic ${auth}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: 'Draft from my local builder',
content: '<p>This came from the tool.</p>',
status: 'draft'
})
});
const data = await response.json();
console.log(response.status, data);
Small win: the first 201 response felt great. The draft showed up in WordPress, assigned to the user behind the Application Password. No browser login. No cookie juggling. No nonce handling. Just a clean REST request.
What WordPress does with that header
When the request hits WordPress, it checks the Basic Auth header. It looks at the username, then compares the provided Application Password against the hashed passwords stored for that user. If the check passes, WordPress treats the request as authenticated for that user.
The password itself is not stored in plain text. WordPress stores a hashed version, plus metadata like the app name, when it was created, and when it was last used. That last-used data is handy when you come back later and wonder, “Is this old integration still alive?” You can see which Application Passwords are being used and revoke the stale ones.
The REST API then handles the request like any other authenticated request. If the user can create posts, the post creation request can work. If the user cannot publish posts, asking for status: publish will fail or behave according to that role’s capabilities. WordPress is not giving the password special powers. It is only proving the user identity for the API call.
Cookies, nonces, JWT, and Application Passwords
I had to sort this out because WordPress auth can feel like a junk drawer when you are moving fast. There are several ways to authenticate, and each one fits a different shape of project.
| Auth method | Best fit | What I watch out for |
|---|---|---|
| Cookies and nonces | Code running inside the WordPress admin or front end for logged-in users | Not ideal for external tools because the browser session is part of the setup |
| Application Passwords | External scripts, small apps, integrations, local tools, server-to-server calls | Permissions follow the user role, so choose the user carefully |
| JWT plugins | Custom apps where token-based login makes sense | Adds plugin dependency and more auth surface to maintain |
| OAuth | Larger apps with user authorization flows | More setup than I wanted for a simple publishing tool |
For this build, Application Passwords were the least dramatic option. I did not need users to authorize access through a polished app screen. I needed a maker-friendly path: generate a password, paste it into the tool, test the API call, then keep shipping.
The permission trap
The biggest mistake is treating an Application Password like a scoped API key. It is not that. If you create one for an admin user and paste it into a script on your laptop, that script can do admin-level things through the REST API where endpoints allow it.
My first pass ignored this because I was in prototype mode. Then I pictured accidentally committing the password to a repo, and that fixed my attitude fast. I created a dedicated user called something like “Draft Bot” and gave it only the role needed for the workflow. For a tool that only creates draft posts, Author may be enough. For a tool that edits custom post types, you may need a custom role with specific capabilities.
- Use a separate user for each external tool when possible.
- Name every Application Password clearly.
- Do not reuse one password across multiple scripts or apps.
- Store the password in environment variables or a secret manager, not in source code.
- Revoke passwords you no longer use.
The bugs I hit while wiring it up
The first bug was boring: wrong username. I used the display name instead of the actual login username. WordPress wanted the login. Once I fixed that, the request moved from unauthorized to a different error, which felt like progress.
The second bug was the password copy. WordPress displays the generated password in chunks so humans can read it. Depending on how you paste it, you may include spaces. WordPress is generally forgiving here, but tooling around environment files can be less forgiving when quotes, spaces, and shell parsing get involved. I ended up storing the app password as one quoted value and logging only whether it existed, never the value itself.
The third bug came from the REST payload. Auth was working, but the post still failed because I was sending a field WordPress did not accept for that user and post type. That was a useful reminder: a 401 points at auth, a 403 points at permission, and a 400 often points at the shape of the request.
if (response.status === 401) {
console.log('Auth failed. Check username and Application Password.');
}
if (response.status === 403) {
console.log('Authenticated, but this user does not have permission.');
}
if (response.status === 400) {
console.log('The request body is probably invalid.');
}
That little status check saved me from chasing the wrong problem. I kept it in the tool because future me deserves fewer mysteries.
Testing with the smallest possible call
Before creating posts, I like to test against the current user endpoint. It answers a simple question: “Who does WordPress think I am?” Not as a section opener, just as a debugging sanity check. If that request works, the auth layer is good and I can move on to permissions and payloads.
const response = await fetch(`${siteUrl}/wp-json/wp/v2/users/me`, {
headers: {
'Authorization': `Basic ${auth}`
}
});
console.log(await response.json());
When that returned the expected user, I knew the rest of the build was no longer an auth problem. That is a nice line to draw. Auth bugs can eat a whole afternoon if every endpoint is tested at once.
How I store the credential during local development
For the local version, I used an environment file that never gets committed. The repo includes an example file with empty values, and the real file stays on my machine. Simple, but it works for a solo builder project.
WP_SITE_URL=""
WP_USERNAME="draftbot"
WP_APP_PASSWORD="xxxx xxxx xxxx xxxx xxxx xxxx"
If this became a hosted app, I would store the password encrypted at rest and make rotation easy from the UI. I would also avoid showing the full value again after save. The builder version can be scrappy. The production version needs guardrails.
Revoking and rotating access
The revocation story is one of the reasons I like this approach. If a laptop gets replaced, a contractor leaves, or a tool is retired, you do not need to change the actual WordPress account password. You go back to the user profile, find the named Application Password, and revoke it.
Rotation is also low drama. Generate a new password, update the tool, test it, then revoke the old one. For my own tools, I keep a short note next to the app name so I know what each password belongs to. Future cleanup gets much easier when the names are not vague.
Security habits that felt reasonable
I did not try to turn a tiny project into a bank vault, but I did want sane defaults. These are the habits I kept:
- Only use Application Passwords over HTTPS.
- Create a dedicated WordPress user for the integration.
- Give that user the lowest role that still lets the tool do its job.
- Keep secrets out of Git.
- Log response status codes, not credentials.
- Rotate passwords when a tool changes hands or moves environments.
- Revoke old passwords instead of letting them pile up.
One more practical detail: Application Passwords can work alongside two-factor authentication because they are meant for non-interactive API access. That does not make them magic. It means you should treat each generated password like a sensitive credential with direct API access.
Where this fits in my builder workflow
After the auth piece worked, the whole tool got more fun. I could generate a draft from a local command, push structured content into WordPress, and let the WordPress editor handle the final pass. The API stopped feeling like a separate system and started feeling like a remote control for the site.
The best part was how little ceremony it took. No plugin install. No custom endpoint just to authenticate. No browser session. WordPress already had what I needed. The main job was understanding the boundaries and not being sloppy with permissions.
If you are building a small external tool for WordPress, I would start here. Create a dedicated user, generate one Application Password, test /wp-json/wp/v2/users/me, then try the smallest write action your tool needs. Keep the feedback loop tight. When something breaks, separate auth from permissions from payload shape. That one habit makes the whole thing feel manageable.
My final setup is not fancy, and that is why I like it. A named password, a limited user, a clean Authorization header, and a few careful checks around errors. Enough to build with confidence, without turning a weekend tool into an identity platform.