
What is the WordPress REST API and how to use it?
The WordPress REST API is a built-in interface that lets other applications read and write your WordPress content over HTTP using JSON. Instead of loading a full themed page, a script, mobile app, or JavaScript front end can request something like /wp-json/wp/v2/posts and get back clean, structured data. It ships with every WordPress install, and the block editor itself uses it to save your posts.
If you have ever wanted to show your WordPress posts in a React app, sync content with another system, or build a custom dashboard, the REST API is the tool that makes it possible. In this article you will learn what the REST API is, how its routes and endpoints are organized, how to make requests with and without authentication, and how to register your own custom endpoints safely.
What Is a REST API?
A REST API (Representational State Transfer Application Programming Interface) is a set of URLs that respond to standard HTTP methods. Each URL represents a resource, such as a post, a user, or a category, and the HTTP method tells the server what you want to do with it:
- GET: Read data (for example, fetch a list of posts).
- POST: Create new data (for example, publish a new post).
- PUT / PATCH: Update existing data.
- DELETE: Remove data.
The server replies with JSON, a lightweight text format that almost every programming language can parse. That is what makes REST APIs so universal: a Python script, a Swift app, and a browser can all talk to the same endpoint.
What Is the WordPress REST API?
The WordPress REST API is WordPress's own implementation of this idea. It was merged into core in WordPress 4.7 and has been expanded in nearly every release since. Today it powers the block editor, the Site Editor, many plugin settings screens, and countless headless and mobile projects.
Out of the box it exposes most of your site's core data, including:
- Posts, pages, and custom post types (when registered with
show_in_rest) - Categories, tags, and custom taxonomies
- Media attachments
- Users
- Comments
- Settings, block types, block patterns, templates, and more
Routes, Endpoints, and Namespaces
Three terms come up constantly when you work with the API:
- Route: The URL path, such as
/wp/v2/posts. - Endpoint: A route combined with an HTTP method.
GET /wp/v2/postsandPOST /wp/v2/postsare two endpoints on the same route. - Namespace: The first part of a route that groups related endpoints and includes a version, such as
wp/v2for core ormyplugin/v1for your own code.
All routes live under the API root, which is https://example.com/wp-json/ on sites with pretty permalinks. If permalinks are set to "Plain", you can use https://example.com/?rest_route=/ instead.
Discovering Available Routes
Visit https://example.com/wp-json/ in your browser and you will see a large JSON document listing every registered namespace and route on your site, including those added by plugins such as WooCommerce or Yoast SEO. This "index" is a great way to explore what your site exposes.
How to Make Your First Request
The simplest way to test the API is to open a public endpoint in your browser. Try:
https://example.com/wp-json/wp/v2/posts
You will get an array of your ten most recent published posts. Each object contains fields like id, date, slug, link, title.rendered, content.rendered, and excerpt.rendered.
From the command line, curl works just as well:
curl https://example.com/wp-json/wp/v2/posts?per_page=5
Useful Query Parameters
Most collection endpoints accept parameters that filter and shape the response:
per_page: Number of items to return (maximum 100).page: Which page of results to return.search: Full-text search.categories/tags: Filter by term IDs.orderbyandorder: Sort results, for exampleorderby=date&order=asc._fields: Return only the fields you need, which makes responses much smaller._embed: Include related resources, such as the featured image and author, in one response.
For example, this request returns only the ID, title, and link of three posts in category 5:
curl "https://example.com/wp-json/wp/v2/posts?categories=5&per_page=3&_fields=id,title,link"
Pagination Headers
Collection responses include two headers that tell you how much data exists:
X-WP-Total: The total number of items.X-WP-TotalPages: The total number of pages at the currentper_pagevalue.
You can view them with curl -I. They are essential when you need to loop through every post on a large site.
Using the REST API with JavaScript
In the browser or Node.js, the native fetch() function is all you need to read public data:
async function getLatestPosts() {
const response = await fetch(
"https://example.com/wp-json/wp/v2/posts?per_page=5&_fields=id,title,link",
);
if (!response.ok) {
throw new Error(`Request failed with status ${response.status}`);
}
const posts = await response.json();
posts.forEach((post) => {
console.log(post.id, post.title.rendered, post.link);
});
}
getLatestPosts();
Keep in mind that title.rendered and content.rendered contain HTML. If you insert them into a page, treat them as HTML from a trusted source (your own site) and avoid mixing them with untrusted user input.
Using apiFetch Inside WordPress
If you are writing JavaScript that runs inside the WordPress admin or a block, use the @wordpress/api-fetch package instead of raw fetch(). It automatically adds the correct root URL and the REST nonce for the logged-in user:
import apiFetch from "@wordpress/api-fetch";
apiFetch({ path: "/wp/v2/posts?per_page=3" }).then((posts) => {
console.log(posts);
});
Authentication: Reading vs. Writing Data
Public content like published posts can be read by anyone. But creating, updating, or deleting content, or reading private data such as drafts and user emails, requires authentication. WordPress offers a few options.
Cookie Authentication (Logged-In Users)
When JavaScript runs on your own site for a logged-in user, WordPress uses the existing login cookie plus a nonce to verify the request. You pass the nonce in an X-WP-Nonce header. In PHP you can hand the nonce to your script like this:
function sajjad_enqueue_rest_script() {
wp_enqueue_script(
'sajjad-rest-demo',
get_stylesheet_directory_uri() . '/js/rest-demo.js',
array(),
'1.0.0',
true
);
wp_localize_script(
'sajjad-rest-demo',
'sajjadRest',
array(
'root' => esc_url_raw( rest_url() ),
'nonce' => wp_create_nonce( 'wp_rest' ),
)
);
}
add_action( 'wp_enqueue_scripts', 'sajjad_enqueue_rest_script' );
Then in rest-demo.js:
fetch(sajjadRest.root + "wp/v2/posts", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-WP-Nonce": sajjadRest.nonce,
},
body: JSON.stringify({
title: "Created via the REST API",
status: "draft",
}),
})
.then((res) => res.json())
.then((post) => console.log("Created post", post.id));
The nonce action must be exactly wp_rest, otherwise WordPress treats the request as unauthenticated.
Application Passwords (External Apps)
For scripts and apps that run outside WordPress, the built-in option since WordPress 5.6 is Application Passwords. To create one:
- Open your profile: Go to Users > Profile (or edit another user's profile as an administrator).
- Find the section: Scroll down to Application Passwords.
- Name it: Enter a descriptive name such as "Deploy script" and click Add New Application Password.
- Copy it: WordPress shows the password once. Store it somewhere safe.
You then use HTTP Basic Authentication with your username and the application password. Application Passwords require HTTPS on production sites.
curl -X POST https://example.com/wp-json/wp/v2/posts \
-u "sajjad:abcd efgh ijkl mnop qrst uvwx" \
-H "Content-Type: application/json" \
-d '{"title":"Hello from curl","status":"draft"}'
Each application password can be revoked individually, so you never have to share your real login password with a third-party tool.
Other Options
For more complex setups, plugins such as JWT Authentication for WP REST API or OAuth-based solutions exist. For most sites, cookie authentication for in-site scripts and Application Passwords for external tools cover everything you need.
Common Tasks with the REST API
Here are a few practical examples you can adapt.
Update a Post
curl -X POST https://example.com/wp-json/wp/v2/posts/42 \
-u "sajjad:your-application-password" \
-H "Content-Type: application/json" \
-d '{"title":"An updated title"}'
WordPress accepts POST, PUT, or PATCH for updates on core endpoints.
Delete a Post
curl -X DELETE "https://example.com/wp-json/wp/v2/posts/42" \
-u "sajjad:your-application-password"
By default this moves the post to the trash. Add ?force=true to delete it permanently.
Upload Media
curl -X POST https://example.com/wp-json/wp/v2/media \
-u "sajjad:your-application-password" \
-H "Content-Disposition: attachment; filename=photo.jpg" \
-H "Content-Type: image/jpeg" \
--data-binary @photo.jpg
Exposing Custom Post Types and Fields
Custom post types only appear in the API if you register them with show_in_rest set to true. This is also required for the block editor to work with them:
function sajjad_register_book_post_type() {
register_post_type(
'book',
array(
'label' => __( 'Books', 'sajjad' ),
'public' => true,
'show_in_rest' => true,
'rest_base' => 'books',
'supports' => array( 'title', 'editor', 'thumbnail', 'custom-fields' ),
)
);
}
add_action( 'init', 'sajjad_register_book_post_type' );
Your books are now available at /wp-json/wp/v2/books.
To expose post meta, register it with register_post_meta():
function sajjad_register_book_meta() {
register_post_meta(
'book',
'isbn',
array(
'type' => 'string',
'single' => true,
'show_in_rest' => true,
'sanitize_callback' => 'sanitize_text_field',
'auth_callback' => function () {
return current_user_can( 'edit_posts' );
},
)
);
}
add_action( 'init', 'sajjad_register_book_meta' );
The isbn value will appear inside the meta object of each book response, and authorized users can update it through the API.
How to Create a Custom REST API Endpoint
When core endpoints are not enough, you can register your own routes with register_rest_route(). Always hook this into rest_api_init. Place the code in a custom plugin (recommended) or your child theme's functions.php.
<?php
/**
* Plugin Name: Sajjad Custom Endpoints
* Description: Adds a custom REST API endpoint for recent post titles.
* Version: 1.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
function sajjad_register_routes() {
register_rest_route(
'sajjad/v1',
'/recent-titles',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => 'sajjad_get_recent_titles',
'permission_callback' => '__return_true',
'args' => array(
'count' => array(
'type' => 'integer',
'default' => 5,
'minimum' => 1,
'maximum' => 20,
'sanitize_callback' => 'absint',
),
),
)
);
}
add_action( 'rest_api_init', 'sajjad_register_routes' );
function sajjad_get_recent_titles( WP_REST_Request $request ) {
$posts = get_posts(
array(
'numberposts' => $request->get_param( 'count' ),
'post_status' => 'publish',
)
);
$data = array_map(
function ( $post ) {
return array(
'id' => $post->ID,
'title' => get_the_title( $post ),
'link' => get_permalink( $post ),
);
},
$posts
);
return rest_ensure_response( $data );
}
Visit /wp-json/sajjad/v1/recent-titles?count=3 and you will see your three newest posts.
Protecting Endpoints with permission_callback
The permission_callback argument is required. Using __return_true makes an endpoint public, which is fine for public data. For anything that changes data or exposes private information, check capabilities:
function sajjad_register_protected_route() {
register_rest_route(
'sajjad/v1',
'/clear-cache',
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => 'sajjad_clear_cache',
'permission_callback' => function () {
return current_user_can( 'manage_options' );
},
)
);
}
add_action( 'rest_api_init', 'sajjad_register_protected_route' );
function sajjad_clear_cache( WP_REST_Request $request ) {
wp_cache_flush();
return new WP_REST_Response(
array( 'cleared' => true ),
200
);
}
If the current user lacks the capability, WordPress automatically returns a 401 or 403 error.
Returning Errors
Return a WP_Error from your callback when something goes wrong. WordPress converts it into a JSON error response with the status code you specify:
return new WP_Error(
'sajjad_not_found',
__( 'The requested item was not found.', 'sajjad' ),
array( 'status' => 404 )
);
Should You Disable the REST API?
You may see advice to disable the REST API for security. Turning it off entirely will break the block editor, many plugins, and the Site Editor, so it is rarely a good idea. A better approach is to restrict specific data. For example, if you do not want the user list to be publicly enumerable, you can require authentication for REST requests from logged-out visitors on the users endpoint:
function sajjad_restrict_users_endpoint( $endpoints ) {
if ( ! is_user_logged_in() ) {
unset( $endpoints['/wp/v2/users'] );
unset( $endpoints['/wp/v2/users/(?P<id>[\d]+)'] );
}
return $endpoints;
}
add_filter( 'rest_endpoints', 'sajjad_restrict_users_endpoint' );
Security plugins such as Wordfence and Solid Security also offer settings to limit REST API access without breaking core features.
Best Practices for Working with the REST API
Use your own namespace: Always prefix custom routes with a unique, versioned namespace like yourplugin/v1 so they never clash with core or other plugins.
Never skip permission checks: Every write endpoint should verify capabilities with current_user_can().
Validate and sanitize input: Use the args array with type, validate_callback, and sanitize_callback rather than trusting request data.
Request only what you need: Use _fields to trim responses and reduce bandwidth, especially for mobile apps and headless front ends.
Cache on the client side: If you are fetching the same data repeatedly, cache it in your app or use a caching layer to avoid hammering your server.
Use HTTPS: Credentials sent with Basic Authentication must always travel over an encrypted connection.
FAQ: WordPress REST API
Yes. The REST API is part of WordPress core and is active on every site. You can see it by visiting /wp-json/ on your domain.
No. Reading, creating, and updating content works without any plugin. Application Passwords for external authentication are also built into core.
The post type must be registered with show_in_rest set to true. Without it, WordPress does not create REST routes for that post type.
The REST API is built into core and uses many fixed endpoints. WPGraphQL is a free plugin that adds a single GraphQL endpoint where you request exactly the fields you need in one query.
The per_page parameter accepts a maximum of 100. For more items, use the page parameter and the X-WP-TotalPages header to loop through the results.
Public endpoints only expose data that is already public on your site. Write operations require authentication and proper capabilities, so security depends mainly on strong passwords, HTTPS, and correct permission callbacks in custom code.
This happens when the X-WP-Nonce header is missing, expired, or created with the wrong action. Generate the nonce with wp_create_nonce('wp_rest') and send it with every authenticated request.
Conclusion
The WordPress REST API turns your site into a flexible content service that any application can talk to. With a few HTTP requests you can read posts, publish content from external tools, upload media, and power completely separate front ends, all while WordPress continues to handle editing, users, and permissions.
Start by exploring the /wp-json/ index on your own site and making a few GET requests. Once you are comfortable, try Application Passwords for authenticated requests, and then register a small custom endpoint in a plugin. With solid permission checks and input validation, the REST API becomes one of the most powerful tools in your WordPress toolkit.


