The WordPress REST API: A Developer's Guide
What the WordPress REST API does, how routes and controllers fit together, authentication options, and how to expose data without opening holes.

The REST API has been part of WordPress core since 4.7, and it's the reason WordPress can be a backend for a React app, a mobile app, another website, or an internal dashboard. It's also enabled by default, which means it's part of your site's surface area whether you use it or not.
This guide covers how it's structured, how to extend it properly, and the things that bite people once an endpoint is in production.
How the API is put together
Everything lives under /wp-json/. Core content sits in the wp/v2 namespace: /wp-json/wp/v2/posts, /pages, /media, /users, /categories and so on. Plugins add their own namespaces — WooCommerce uses wc/v3, for example.
A route is a URL pattern. An endpoint is a route plus a method, so /wp-json/wp/v2/posts responds differently to GET and POST. Each endpoint has a callback that does the work, a permission callback that decides whether the caller is allowed, and an args definition that validates and sanitises the input.
That last part is worth dwelling on. The args definition is where you declare each parameter's type, whether it's required, how it's sanitised and how it's validated. Using it properly means your callback can trust its input, which is most of the security story.
Registering your own route
Register routes on the rest_api_init hook, in your own namespace with a version in it — my-site/v1 rather than adding to wp/v2. Versioning the namespace means you can change the shape of a response later without breaking whatever is consuming it.
A route needs four things to be production-ready: a specific method rather than a catch-all, a permission_callback that genuinely checks something, an args array that validates every parameter, and a response shaped deliberately rather than returning whatever a query gave you.
Never set permission_callback to __return_true without meaning it. WordPress will warn you if you omit it entirely, and the temptation is to silence the warning with a blanket allow — which is exactly how private data ends up public.
Authentication options, and when each fits
Whatever you choose, it only holds up over HTTPS. Application passwords and basic auth over plain HTTP hand your credentials to anyone on the network.
- Cookies plus a nonce — for JavaScript running inside your own WordPress pages. Simple, and the default for the block editor. Useless for anything outside the site.
- Application passwords — built into core since 5.6. Per-user, revocable credentials sent over HTTPS with basic auth. The right default for server-to-server integrations and scripts.
- OAuth — for third-party applications acting on behalf of a user, where you don't want to hand over credentials. More setup, and usually more than a single integration needs.
- JSON Web Tokens — common in headless setups with a separate frontend that logs users in. Needs a plugin, and needs care around token expiry, refresh and storage.
Reading data without over-fetching
The core endpoints return a lot per item. A list of twenty posts with rendered content, excerpts, meta and links is a large response for a frontend that only needs titles and dates.
- Use the _fields parameter to ask for only the fields you need. It's the single easiest performance win.
- Use _embed sparingly — it saves round trips but makes each response much larger.
- Respect per_page limits and paginate properly using the X-WP-Total and X-WP-TotalPages headers rather than fetching everything.
- For a specific screen in an app, a purpose-built endpoint returning exactly that screen's data beats three core requests stitched together.
Exposing custom post types and fields
A custom post type is only in the API if it was registered with show_in_rest set to true and a rest_base given. The same applies to taxonomies. If a post type is missing from /wp-json/wp/v2, that's almost always why.
Custom fields need registering too. register_post_meta() with show_in_rest exposes a meta key, and it takes a type, a sanitisation callback and an auth callback for writes. Exposing meta by simply making it public is how internal fields end up readable by anyone.
For anything more structured than a scalar, register a REST field with register_rest_field() so you control exactly what is returned and what a write is allowed to do.
Performance in front of the API
REST responses are generated by PHP on every request and aren't page-cached by default. A frontend polling an endpoint is a frontend hammering your database.
- Cache expensive responses with transients, keyed by the parameters that affect them, and invalidate on save_post rather than waiting for expiry.
- Put a CDN or reverse proxy in front of genuinely public, cacheable GET endpoints and set sensible cache headers.
- Watch for N+1 queries inside a response callback — a loop that calls get_post_meta() per item is the usual culprit.
- Add rate limiting for public write endpoints. Without it, a form endpoint is a spam endpoint.
Hardening the API surface
The API's default behaviour leaks more than most sites intend. /wp-json/wp/v2/users lists author accounts with their slugs, which is a usable list of usernames for a brute-force attempt. On a site that doesn't need public user data, restrict that endpoint.
- Restrict or disable endpoints you don't use, especially users.
- Require authentication for the whole API on sites with no public consumer, using the rest_authentication_errors filter.
- Check permission callbacks against capabilities (current_user_can) rather than roles, so custom roles behave correctly.
- Log and monitor write endpoints. An endpoint creating posts should be boring; a spike in traffic to it isn't.
- Don't disable the API wholesale — the block editor and several core features depend on it. Restrict rather than remove.
Common mistakes
- Returning raw database rows instead of a deliberate response shape, which couples your API to your schema forever.
- Forgetting that rendered content contains HTML that a non-WordPress frontend must handle or sanitise itself.
- Assuming the API respects the same caching layer as the site. It usually doesn't.
- Building integrations that poll every minute when a webhook would do the job.
- No versioning, so the first change to a response breaks the consumer silently.
When the REST API is the right tool
It's the right tool when something outside the theme needs your content: a React or Next.js frontend, a mobile app, a partner site syndicating articles, an internal dashboard, or a CRM that needs to know when someone submits a form.
It's the wrong tool when you're fetching data for the same page WordPress is already rendering. A query in the template is faster and simpler than an API call from the browser back into the site it came from.
If you're planning an integration and want a second opinion on the endpoint design before it's built, send me an outline of what needs to talk to what.
Frequently asked questions
Should I disable the WordPress REST API?
No — the block editor and several core features depend on it, and disabling it wholesale breaks the admin. What's worth doing is restricting it: require authentication if nothing public consumes it, and limit the users endpoint, which otherwise publishes a list of your account slugs.
Is the WordPress REST API secure?
The framework is sound; the risk is in how endpoints are registered. The two recurring mistakes are a permission_callback that always returns true, and custom meta exposed without an auth callback so anyone can write to it. Validate every parameter through the args definition and check capabilities rather than roles.
REST API or WPGraphQL?
REST is in core, needs no plugin and is simpler to cache at the edge. WPGraphQL lets a client ask for exactly the fields it needs in one request, which suits complex frontends fetching deeply related data. For most sites REST plus a few purpose-built endpoints is less machinery for the same result.
Why is my custom post type missing from the REST API?
Almost always because it was registered without show_in_rest set to true. Add that, and a rest_base if you want a nicer URL than the post type key. The same applies to custom taxonomies, and separately to custom fields, which need register_post_meta with show_in_rest before they appear.
How do I authenticate a script against the WordPress REST API?
Application passwords, built into core since 5.6, are the right default. Generate one per integration from the user's profile screen, send it with basic auth over HTTPS, and revoke it independently if it leaks. Don't reuse the account password, and don't put credentials in client-side code.
Topics
- WordPress REST API
- WordPress API development
- wp-json
- custom REST endpoint