WordPress REST API: A Developer’s Field Guide

Abstract technical illustration for the article: WordPress REST API: A Developer's Field Guide

What the REST API actually is

Every WordPress site you install today ships with a JSON API built in. Request /wp-json/ on any modern WordPress install and you get a machine-readable index of everything the site exposes: posts, pages, media, users, comments, taxonomies, and any custom routes registered by themes and plugins. That API is the WordPress REST API, and it quietly underpins the block editor, mobile apps, and an entire ecosystem of headless WordPress front ends.

The API did not arrive all at once. Its infrastructure was merged into WordPress core in version 4.4 (December 2015), but the content endpoints — the routes developers actually use, like posts and pages — only landed in core with version 4.7 in December 2016. Before that, the full API existed as the WP REST API feature plugin. Since 4.7, it has been part of every WordPress install by default, and the block editor (Gutenberg) itself talks to the server through these same endpoints whenever you save a post. When you understand the REST API, you understand the machinery beneath the modern WordPress admin.

The core endpoints

The built-in routes live under the wp/v2 namespace. The ones you will reach for constantly are:

  • /wp-json/wp/v2/posts — published posts; supports GET, POST, PUT/PATCH, DELETE
  • /wp-json/wp/v2/pages — pages, same verbs
  • /wp-json/wp/v2/media — the media library; POST here uploads files
  • /wp-json/wp/v2/users — user accounts (write access restricted, and user listing is intentionally limited)
  • /wp-json/wp/v2/comments — comments
  • /wp-json/wp/v2/categories and /wp-json/wp/v2/tags — taxonomies and their terms
  • /wp-json/wp/v2/types, /wp-json/wp/v2/statuses, /wp-json/wp/v2/search — discovery helpers

A quick sanity check from the command line shows the shape of the data:

curl https://example.com/wp-json/wp/v2/posts?per_page=3 | head -c 600

Three behaviours are worth internalising early. First, responses are paginated: the default page size is 10, and you can request up to 100 per page with per_page. The totals arrive in response headers — X-WP-Total and X-WP-TotalPages — so paginate by reading headers, not by guessing. Second, related resources (author, featured media, terms) are embedded on demand with ?_embed, which saves you a fan-out of follow-up requests. Third, on sites without pretty permalinks enabled, the API does not live at /wp-json/ at all: routes are passed as the rest_route query parameter instead, e.g. https://example.com/?rest_route=/wp/v2/posts. Use rest_url() in PHP rather than hard-coding the path.

Authentication: three ways in

Reading public content needs no credentials at all. Everything else — creating posts, uploading media, reading drafts — requires the request to prove who it is. WordPress offers three practical paths.

1. Cookie authentication with nonces

If your code runs inside the WordPress admin itself, you are already authenticated: the browser holds the login cookie. Cross-site request forgery is handled with a nonce — a one-time token generated with wp_create_nonce( 'wp_rest' ) and sent back in the X-WP-Nonce header. This is exactly how the block editor authenticates its own API calls, and it is the right choice for admin-side JavaScript. It is not usable from external applications, since third-party code has no way to mint the nonce.

2. Application passwords

Application Passwords, introduced in WordPress 5.6 (December 2020), are the recommended way for external scripts and services to authenticate. From a user's profile page in wp-admin you generate a revocable, per-application credential — a long random string tied to that specific user account. The credential is stored hashed, shown only once at creation time, and can be revoked individually without changing the user's main password.

Requests authenticate with plain HTTP Basic Auth (RFC 7617), which means the site must be served over HTTPS:

curl --user "username:abcd efgh ijkl mnop" https://example.com/wp-json/wp/v2/users?context=edit

Application passwords cannot be used to log in through wp-login.php; they are strictly for API-style access. That limitation is a feature: even if a credential leaks, it buys the attacker API access scoped to one user's capabilities, not a wp-admin session. Use descriptive names per integration (“deployment bot”, “mobile app”) so you can revoke precisely the right one later.

3. OAuth and plugin-based auth

For delegated access — “let this third-party app act on my site” — look at OAuth. The handbook points to plugins such as the OAuth 1.0a Server and JSON Web Tokens implementations. There is also a Basic Authentication plugin, but the handbook is explicit that it sends your real username and password with every request and should only be used for local development and testing. In production, prefer Application Passwords or a proper OAuth flow.

Building custom routes

The core endpoints cover the built-in content types, but real projects need their own. Custom routes are registered with register_rest_route(), always inside a callback on the rest_api_init action so the registration only runs when the API is loaded. A route has three ingredients: a namespace (yours — version it, e.g. myplugin/v1), the route pattern, and an options array with the HTTP methods, a callback, and a permission callback.

add_action( 'rest_api_init', function () {
register_rest_route( 'myplugin/v1', '/stats', array(
'methods' => WP_REST_Server::READABLE,
'callback' => 'myplugin_get_stats',
'permission_callback' => function () {
return current_user_can( 'edit_posts' );
},
) );
} );

function myplugin_get_stats( WP_REST_Request $request ) {
return rest_ensure_response( array(
'drafts' => count( get_posts( array( 'post_status' => 'draft', 'numberposts' => -1 ) ) ),
) );
}

The permission_callback deserves emphasis: it is where your endpoint's security lives. Returning __return_true makes the route public, which is fine for genuinely public data but a classic mistake for anything sensitive. Check capabilities with current_user_can(), and note that the API verifies cookie nonces for you automatically — you do not need to validate them inside custom endpoints.

For non-trivial APIs, declare argument schemas. Each field in the options array can specify args with defaults, sanitization callbacks, validation callbacks, and required flags. Routes also accept a schema callback describing the response shape; the API then answers OPTIONS requests with that schema, which is how documentation generators and typed clients discover your endpoints. If you outgrow function-based registration, the handbook documents controller classes: extend the pattern with get_items, get_item, permission checks, and a shared get_item_schema(), which is exactly how the core endpoints themselves are built.

Going headless

“Headless” or “decoupled” WordPress means using WordPress purely as a content backend and rendering the front end elsewhere — a Next.js or Astro site, a mobile app, a kiosk display. The REST API is the wire between the two. The pattern is straightforward: content editors keep the familiar WordPress admin, and the front end fetches published content at build time or request time from /wp-json/wp/v2.

A few things make or break headless setups. First, decide how previews and drafts reach the front end: private content needs authenticated requests, so your front end must hold an application password for a user with the right capabilities, and that credential must never ship to the browser. Second, think about cache invalidation — static front ends go stale, so many teams use a webhook or a custom route to trigger rebuilds when content changes. Third, audit what the API exposes: post revisions, author data, and anything your plugins add to responses all travel over the wire. The REST API is not a security boundary on its own; your permission callbacks and content model are.

Practical pitfalls

  • The N+1 trap. Fetching 20 posts then requesting each author's details separately is 21 requests. Use _embed or batch your reads instead.
  • User enumeration. The users endpoint exposes usernames by design. If that bothers you, restrict it or filter the response — do not be surprised by it.
  • Unauthenticated writes. If your custom route lacks a permission callback, it is public. Write the permission check first, then the callback logic.
  • Ignoring response headers. Pagination, rate information, and total counts live in headers. Parse them.
  • Assuming pretty permalinks. Client code should derive the API root from the /wp-json/ index or use rest_url() in PHP, with the ?rest_route= fallback in mind.
  • No caching strategy. The API hits the database like any other WordPress request. Cache API responses at the HTTP layer (or in your front end) for anything read-heavy.

The REST API rewards the same discipline as the rest of WordPress development: declare your schema, check your permissions, and treat every endpoint as a public contract. Do that, and WordPress stops being just a CMS and becomes a genuinely capable application backend.

Further reading

Similar Posts

Leave a Reply