What the WordPress REST API Is
The WordPress REST API is a built in interface that exposes your site content as data over HTTP. Instead of asking WordPress to render an HTML page, an application asks for the raw information, a post, a page, a user, a category, and gets it back as JSON. JSON is a plain text format that almost every programming language can read, which is why it has become the common tongue of the web.
Here is the mental shift that matters. Classic WordPress mixes content and presentation. A theme template loops through posts and prints HTML. The REST API splits those two jobs apart. WordPress becomes the place where content is stored, edited, and managed, and something else, a JavaScript app, a mobile client, another server, decides how to present it. Your content stops being trapped inside one theme.
You are already using this API even if you have never called it directly. The block editor talks to the REST API every time you save a post. Many plugins read and write through it. The mobile apps for WordPress use it. So the question is rarely whether to use the API, it is how deliberately you use it for the features you are building.
Why developers reach for it
The appeal comes down to a few concrete things. You keep the WordPress editing experience that clients already know, while gaining freedom in how the front end is built. You can serve the same content to a website, an app, and a partner integration from one source of truth. And because the format is standard JSON over standard HTTP, the skills transfer, any developer who has consumed an API can consume this one.
There is a cost too, and it is fair to name it. Going API first, especially in a headless build, means you take on work that a normal theme handles for you, such as routing, previews, and SEO output. We will get to that. For now, hold onto the core idea, the REST API is how WordPress content leaves the WordPress template layer and travels anywhere.
A short history worth knowing
The REST API did not always ship in core. For a few years it lived as a feature plugin while the team worked out the shape of the routes and the response format. The infrastructure landed in WordPress 4.4, and the full content endpoints for posts, pages, and the rest arrived in 4.7. That history matters for one practical reason, almost every actively maintained site now has the full API available, so you can rely on it being present without asking a client to install anything. When you read older tutorials that tell you to install a REST API plugin, that advice is out of date for the core content endpoints.
It also helps to know what REST means, because the name is not decoration. REST is a style of designing web interfaces around resources that you act on with standard HTTP methods. A post is a resource. You read it with GET, create it with POST, and remove it with DELETE. That consistency is why a developer who has never touched WordPress can still guess how the API behaves, the conventions are shared across most of the modern web.
How It Works: Routes, Endpoints, JSON
Three words carry most of the meaning here, route, endpoint, and namespace. Get comfortable with them and the rest falls into place.
A route is a URL path the API responds to. A endpoint is a route combined with an HTTP method, so the same route can behave differently for a GET than for a POST. A namespace is a prefix that groups routes and versions them, which keeps different plugins and API versions from colliding.
Everything starts at the base URL. Append /wp-json/ to any WordPress site address and you reach the API root. Core content sits inside the wp/v2 namespace, so a real posts route looks like this:
https://example.com/wp-json/wp/v2/posts
Read that from left to right. The domain is your site. The /wp-json/ segment is the API base. The wp/v2 part is the core namespace and version. The posts part is the resource. Open that URL in a browser on any public WordPress site and you will see a JSON array of the latest posts. Open the bare /wp-json/ root and WordPress returns a discovery document that lists every route registered on the site, which is the fastest way to learn what a given site exposes.
Reading a JSON response
A single post comes back as a JSON object with fields for the id, the date, the slug, the title, the content, the excerpt, the author id, and more. Titles and content arrive as objects with a rendered property, because WordPress runs them through its filters before sending. Knowing that shape saves confusion, when you want the visible title you read title.rendered, not title.
Filtering with query parameters
The API is far more useful once you filter. Query parameters let you narrow, sort, and page through results without writing any server code. A few you will use constantly:
- per_page and page control how many items you get and which page you are on. The maximum per page is 100.
- search returns items matching a term.
- categories and tags filter posts by taxonomy term ids.
- orderby and order sort the results, for example by date descending.
- _fields trims the response to only the fields you name, which is one of the easiest performance wins available.
- _embed pulls in related resources such as the author and featured image so you avoid extra requests.
So a request for the five most recent posts, returning only the title and link, and embedding the featured image, is just a URL with the right parameters attached. No plugin, no custom code, and the response is small and fast.
Core Endpoints You Will Use
Core WordPress registers a set of endpoints under the wp/v2 namespace that cover the content types you already know. Once you learn the pattern, all of them read the same way, a collection route returns many items, and the same route with an id returns a single item.
Here are the endpoints you will use most, with their routes and a note on what each returns.
| Resource | Route | Returns |
|---|---|---|
| Posts | /wp-json/wp/v2/posts | Blog posts, filterable by category, tag, author, and date. |
| Single post | /wp-json/wp/v2/posts/<id> | One post by its numeric id. |
| Pages | /wp-json/wp/v2/pages | Static pages, with parent and menu order fields. |
| Media | /wp-json/wp/v2/media | Attachments, including image sizes and source URLs. |
| Users | /wp-json/wp/v2/users | Authors and users, with public fields for unauthenticated reads. |
| Categories | /wp-json/wp/v2/categories | The category taxonomy terms. |
| Tags | /wp-json/wp/v2/tags | The post tag taxonomy terms. |
| Comments | /wp-json/wp/v2/comments | Comments, readable publicly and writable when allowed. |
| Taxonomies | /wp-json/wp/v2/taxonomies | The taxonomies registered on the site. |
| Post types | /wp-json/wp/v2/types | The post types registered on the site. |
| Settings | /wp-json/wp/v2/settings | Site settings, available only to authenticated admins. |
Posts, pages, and media
Posts are the workhorse. You filter them by category id, tag id, author, search term, and date range, then page through the results. Pages behave almost identically but add a parent field, which matters when you build a nested navigation from page hierarchy. Media is where featured images and uploads live, and each media item exposes several rendered sizes, so a front end can pick the right one for the layout rather than shipping a giant original.
Users and taxonomies
The users endpoint returns public author information without authentication, and fuller data once you log in. Taxonomy endpoints, categories and tags out of the box, let you build filters and archives. Any custom taxonomy registered with show_in_rest set to true gets its own route in the same style, so a product catalogue with custom taxonomies is fully readable through the API.
Custom post types
This is the part that unlocks real projects. When you register a custom post type and pass show_in_rest => true, WordPress exposes it at /wp-json/wp/v2/your-type automatically, with the same query parameters as posts. That means a portfolio, a listings directory, or an events calendar becomes an API resource with no extra endpoint code. If you are planning that kind of build, our development services cover custom post types wired into an API front end, and you can request a quote to get it scoped.
Authentication Methods
Reading public content needs no login. The moment you want to create, update, or delete content, or read anything private, you must authenticate. WordPress checks the same capabilities it uses everywhere else, so a request can only do what the underlying user is allowed to do. There are four practical methods, and choosing the right one is mostly about where the request comes from.
Cookie authentication with a nonce
This is the method WordPress uses for requests made from inside the site while a user is logged in, the block editor being the obvious example. The browser already holds the login cookie. To prove the request is intentional and not forged, WordPress also requires a nonce, a short lived token, sent in the X-WP-Nonce header. If you are writing JavaScript that runs on your own WordPress pages, this is the correct approach and the easiest to set up. It does not work for outside applications, because they do not have the cookie.
Application passwords
Application passwords are built into WordPress core and are the right default for server to server and outside integrations. A user generates a dedicated password for a specific application from their profile screen. That password is sent with standard HTTP Basic authentication, and it never exposes the account main password. You can revoke a single application password without disturbing anything else. The one rule that is not negotiable, only ever send it over HTTPS, because Basic auth puts the credential in the request header.
JWT authentication
JSON Web Tokens suit front end applications that log a user in and then make many requests. A plugin adds a token endpoint, the client sends credentials once, receives a signed token, and then attaches that token in an Authorization: Bearer header on later requests. Tokens expire, which limits exposure if one leaks. JWT needs a plugin because it is not part of core, but it is a clean fit for a React or mobile app with real user sessions.
OAuth
OAuth is the choice when a third party application needs access on behalf of a user without ever seeing that user password, the pattern behind sign in with another service. It is the most involved to set up and usually calls for a plugin and careful configuration. Reach for it when you are granting access to software you do not control, not for your own internal tools.
| Method | Best for | Needs a plugin | How the credential travels |
|---|---|---|---|
| Cookie and nonce | Same site JavaScript, block editor | No | Login cookie plus X-WP-Nonce header |
| Application passwords | Server to server, outside integrations | No | HTTP Basic auth over HTTPS |
| JWT | App front ends with user sessions | Yes | Bearer token in Authorization header |
| OAuth | Third party access on behalf of a user | Yes | Access token after an authorization flow |
If you are unsure which method fits your project, that is a normal question and the answer depends on who is calling the API and from where. We help teams pick and implement the right one. You can get a free quote and we will recommend an approach that matches your security needs.
Making Requests: GET, POST, PUT, DELETE
The API maps neatly onto the four HTTP methods you have probably seen before. GET reads, POST creates, PUT updates, and DELETE removes. The same route handles several of these depending on the method and your permissions, which is exactly what the endpoint idea from earlier describes in practice.
- GET retrieves one or many items and never changes anything. No authentication is needed for public data.
- POST creates a new item, for example a new post. It requires authentication and the right capability.
- PUT updates an existing item. In practice the WordPress API also accepts POST for updates, but PUT expresses intent clearly.
- DELETE removes an item, moving it to trash by default, or permanently when you pass force.
A read request with fetch
From JavaScript in a browser or a Node process, the built in fetch function is all you need for reads. This example asks for the three most recent posts and pulls out the rendered title of each.
fetch('https://example.com/wp-json/wp/v2/posts?per_page=3&_fields=id,title,link')
.then(function (response) {
if (!response.ok) {
throw new Error('Request failed: ' + response.status);
}
return response.json();
})
.then(function (posts) {
posts.forEach(function (post) {
console.log(post.id, post.title.rendered);
});
})
.catch(function (error) {
console.error(error);
});
Notice the _fields parameter trimming the payload and the check on response.ok before reading the body. Both are small habits that make client code faster and less fragile.
A write request with curl
For a quick test from a terminal, or for a server side script, curl with an application password creates a post in one command. The user and application password go in the --user flag, and the body is JSON.
curl --user "editor:xxxx xxxx xxxx xxxx xxxx xxxx" \
-X POST https://example.com/wp-json/wp/v2/posts \
-H "Content-Type: application/json" \
-d '{"title":"Posted through the API","content":"Hello from curl.","status":"draft"}'
Setting status to draft is a good instinct while you test, so nothing publishes by accident. Swap in publish when you mean it. To update that post later, send the same request to /wp-json/wp/v2/posts/<id> with the fields you want to change. To remove it, use -X DELETE on the same single item route.
Reading the response and status codes
Every response carries an HTTP status code, and reading it saves hours of guesswork. A 200 means success on a read, a 201 means something was created, a 400 means your input was malformed, a 401 means you are not authenticated, a 403 means you are authenticated but not allowed, and a 404 means the route or item does not exist. When something fails, WordPress returns a JSON object with a code and a message, which usually tells you exactly what went wrong.
Pagination headers you should read
Collection responses carry two headers that are easy to miss and genuinely useful. X-WP-Total holds the total number of items matching your query across all pages, and X-WP-TotalPages holds how many pages that works out to at your chosen per_page. Read those headers and you can build a correct pager without a second counting request. Ignore them and you end up guessing when to stop, or making extra calls you did not need. Most HTTP clients expose response headers alongside the body, so wiring this in is a few lines, not a rewrite.
Batching related work
When a page needs several pieces of content, resist the habit of firing a separate request for each one in a tight loop. Prefer a single filtered request where you can, use _embed to fold in authors and images, and consider the core batch endpoint for grouped writes. Fewer, larger requests almost always beat many small ones, both for speed and for staying within any rate limits the server enforces. This is the same instinct that keeps a database from drowning in tiny queries, applied one layer up.
Custom Endpoints with register_rest_route
The core endpoints cover content, but real projects need custom behaviour, a route that returns a filtered mix of data, triggers an action, or wraps a third party service. That is what register_rest_route is for. You hook into rest_api_init, declare a namespace and route, and provide a callback that returns the data.
Choose your own namespace rather than reusing wp/v2, so your routes never clash with core or another plugin. A common convention is a short plugin name plus a version, such as myplugin/v1.
<?php
add_action( 'rest_api_init', function () {
register_rest_route( 'myplugin/v1', '/featured/(?P<count>\d+)', array(
'methods' => 'GET',
'callback' => 'myplugin_get_featured',
'permission_callback' => '__return_true',
'args' => array(
'count' => array(
'validate_callback' => function ( $value ) {
return is_numeric( $value ) && (int) $value <= 20;
},
),
),
) );
} );
function myplugin_get_featured( WP_REST_Request $request ) {
$count = (int) $request['count'];
$query = new WP_Query( array(
'posts_per_page' => $count,
'meta_key' => 'featured',
'meta_value' => '1',
) );
$data = array();
foreach ( $query->posts as $post ) {
$data[] = array(
'id' => $post->ID,
'title' => get_the_title( $post ),
'link' => get_permalink( $post ),
);
}
return rest_ensure_response( $data );
}
Walk through the important parts. The route pattern captures a numeric count from the URL. The args block validates that count before your callback ever runs, so bad input is rejected early. The callback receives a WP_REST_Request object, does its work, and returns through rest_ensure_response, which wraps plain data in a proper response object.
The permission callback is not optional
The single most important line above is permission_callback. This example uses __return_true because the data is public, but if your endpoint touches anything private or writes data, you must return a real capability check, for example current_user_can( 'edit_posts' ). Leaving the permission callback off, or setting it to always true on a sensitive route, is one of the most common and most serious mistakes in custom API code. WordPress will even warn about a missing permission callback for exactly this reason.
Validation and sanitization
Two callbacks guard your input. A validate_callback decides whether a value is acceptable and rejects it if not. A sanitize_callback cleans a value into a safe form before you use it. Use validation for numbers, enums, and formats, and sanitization for text and slugs. Together they keep unexpected input from reaching your queries. Never trust a parameter just because it arrived through your own route.
Returning proper errors
When something goes wrong inside your callback, do not return a bare string or let a fatal error leak. Return a WP_Error object with a machine readable code, a human message, and an HTTP status. WordPress turns that into a clean JSON error response with the right status code, which is exactly what a client expects. A callback that returns a well formed error is far easier to consume than one that returns a 200 with an error hidden in the body. Decide your error codes up front and document them alongside your routes, so the front end team can handle each case deliberately rather than guessing from the message text.
Schema and discoverability
A polished endpoint describes itself. Passing a schema callback to your route tells the API what fields it returns and what types they are, which shows up when a client sends an OPTIONS request to the route. You do not have to write a schema to ship a working endpoint, but on an API that other people will build against, it pays for itself in fewer support questions. It also lets the API validate output for you in some cases, which catches mistakes before they reach a client.
Extending existing responses
Sometimes you do not need a whole new route, you just want an extra field on an existing one. register_rest_field adds a field to a core response, for example attaching a computed reading time to every post. It keeps clients on the standard endpoints while giving them the data they need, which is often cleaner than inventing a parallel route.
Headless and Decoupled Front Ends
Headless WordPress is the architecture that made the REST API famous. The idea is to separate the body, the front end that visitors see, from the head, which here is the content management. WordPress keeps doing what it is best at, storing and editing content, and a separate application renders the site by pulling that content through the API.
In a decoupled build, the front end is usually a JavaScript framework such as React, Vue, or Next.js, or a static site generator that fetches content at build time. Editors still log into the familiar WordPress admin. Visitors never touch WordPress directly, they load the front end, which requests JSON from /wp-json and turns it into pages.
Why teams choose headless
- Front end freedom. You build the interface with modern tooling and component libraries, unconstrained by the theme system.
- One content source, many outputs. The same API feeds a website, a mobile app, and partner integrations at once.
- Performance ceiling. A static or heavily cached front end can be extremely fast, since pages are prebuilt or served from the edge.
- Separation of concerns. Front end and back end teams can work in parallel against a shared contract, the API.
The honest trade offs
Headless is powerful, and it is not free. Several things a normal theme gives you become your responsibility. Previewing unpublished content needs extra wiring, because the front end is separate from the editor. SEO output, meta tags, sitemaps, and structured data, has to be reproduced in the front end rather than handled by a plugin in the theme. Redirects, forms, and some plugins that assume a themed front end need rethinking. And you now run and deploy two systems instead of one.
None of that should scare you off, but it should shape the decision. Headless earns its keep on content rich sites that need a custom front end experience or that feed several channels. For a straightforward brochure or blog site, a well built classic or block theme is often the better call, and cheaper to maintain. If you want fast pages without going fully headless, our guide on how to speed up a WordPress website covers a lot of ground first.
Deciding between headless and a traditional build is exactly the kind of question worth a short conversation before you commit code. We do these builds and we will tell you honestly which one fits. You can request a quote and we will lay out both paths for your project.
Security Best Practices
The REST API is as secure as the way you use it. Core respects capabilities and permissions, so the platform is not the weak point. Problems come from configuration and custom code. Here is the list we hold our own builds to.
Serve everything over HTTPS
This is first for a reason. Application passwords and Basic auth put credentials in request headers, and JWT and OAuth pass tokens. Over plain HTTP, all of that is readable in transit. HTTPS is not a nice to have for an API, it is a requirement. If your site is not fully on HTTPS, fix that before you expose any authenticated endpoint.
Always set a real permission callback
As covered in the custom endpoints section, every route you register needs a permission_callback that reflects who should be allowed. For public reads, an explicit always true is fine and honest. For anything that writes or reads private data, check a real capability. Do not skip it and do not default it to true out of convenience.
Validate and sanitize every input
Treat all incoming parameters as untrusted, even on your own routes. Validate formats and ranges, sanitize text and slugs, and use prepared queries so nothing you receive can reach the database unescaped. This is the same discipline you apply anywhere in WordPress, and the API is no exception.
Limit what you expose
By default the users endpoint reveals author names and slugs to anyone, which some sites would rather not publish. You can restrict that, and you can require authentication for routes that do not need to be public. The goal is not to disable the API, the block editor needs it, but to expose only what your project actually requires.
Rate limit and monitor
A public write endpoint invites abuse. Rate limiting at the server or a security plugin slows brute force attempts and scraping. Logging failed authentication and unusual traffic gives you an early warning. On sites that matter, both are worth the small effort. If you want a security review of an API that is already live, our team can audit it, and you can get a free quote for that work.
Performance and Caching
An API that is correct but slow will still frustrate users, and the fixes are mostly straightforward. Performance work on the REST API falls into two buckets, sending less and caching more.
Send less data
The _fields parameter is the simplest win available. If a list view only needs the id, title, and link, ask for only those and skip the full content body on every item. Paginate with per_page rather than pulling hundreds of items at once. Use _embed thoughtfully, it saves round trips by including related data, but it also grows the response, so use it where it genuinely replaces extra requests.
Cache responses
Read heavy endpoints are ideal caching candidates because the same request returns the same data for many users. A few layers help, stacked from the outside in:
- A CDN or edge cache in front of the site serves repeated GET requests without touching WordPress at all.
- Full page or object caching on the server reduces database work for requests that do reach WordPress.
- The WordPress object cache, backed by a persistent store, keeps expensive query results in memory between requests.
Set sensible cache lifetimes based on how often the content changes, and invalidate the cache when a post is saved so readers are not stuck with stale data. Getting invalidation right is the part that takes care, but the payoff is a fast API that shrugs off traffic spikes.
Honour caching headers
The API can work with the browser and any intermediate cache through standard HTTP headers. An ETag or a Last-Modified value lets a client ask whether anything has changed since last time, and receive a small not modified response when it has not, instead of the whole payload again. For a front end that polls for updates, that difference is large over a day of traffic. Set cache control headers that match how fresh each route needs to be, a rarely changing settings route can cache far longer than a live comments feed, and let the layers above WordPress do their job.
Watch your queries
Custom endpoints are only as fast as the queries behind them. Avoid unbounded loops, index the meta you filter on, and be wary of running queries inside a loop over another query. A profiler will show you the expensive calls quickly. The same performance mindset that keeps a normal WordPress site quick applies to the API, and our WordPress speed guide goes deeper on hosting, caching, and query tuning that helps here too.
Common Errors and Fixes
A handful of errors account for most of the time people lose with the REST API. Recognise them and you will move much faster.
rest_no_route (404)
WordPress does not recognise the route you requested. Usually the URL is wrong, a typo in the namespace, a missing version, or the plugin that registers the route is not active. Open the /wp-json/ root and confirm the route is actually listed. If you just registered it in code, make sure the plugin is active and that you hooked into rest_api_init.
rest_forbidden (401 or 403)
You are not allowed to do what you asked. A 401 means the request was not authenticated at all, so check that your credentials or token are being sent and that you are on HTTPS. A 403 means you are authenticated but the user lacks the capability, which points at either the wrong user or a permission callback doing its job. This is also the error you see when a nonce is missing or has expired on a same site request.
rest_cannot_create or rest_cannot_edit
The authenticated user does not have the capability for that write. Confirm the user role can perform the action, and that your custom route permission callback is not blocking it. This is often a role and capability question rather than an API bug. If you keep hitting it with a user you believe should have access, print the user roles and confirm the account you authenticated as is really the one you think it is, mismatched accounts are a frequent cause.
Malformed request (400)
Your input did not pass validation. Check that the JSON body is valid, that the Content-Type header is set to application/json on writes, and that required fields are present and correctly typed. The error message names the parameter at fault more often than people expect, so read it carefully.
CORS errors in the browser
If a front end on a different domain calls the API and the browser blocks it, that is a cross origin restriction, not a WordPress failure exactly. The API sends permissive headers for many cases, but authenticated cross origin requests need the right headers configured on the server. Plan your domains and headers early in a headless build so this does not surprise you near launch.
The API returns HTML instead of JSON
If a request returns an HTML error page rather than JSON, something is intercepting the request before the API runs, often a security plugin, a rewrite rule, or broken permalinks. Re saving permalinks in the WordPress admin fixes a surprising share of these. If that does not help, look at your 404 handling and any plugin that filters requests.
Where to go from here
The WordPress REST API rewards a little upfront learning with a lot of flexibility. Start by reading public endpoints in a browser, then try an authenticated write with an application password, then register a small custom route with a proper permission callback. From there, a headless front end or an app integration is a matter of scale, not a different skill.
If you want any of this built for you, a custom endpoint, a decoupled front end, an integration between WordPress and another system, or a security and performance review of an API you already run, that is our core work. You can see our services or get a free quote and we will scope it around your goals. For related reading, our guides on WordPress SEO and how to make a WordPress website pair well with an API driven build.