The REST API
WordPress can serve content not only as pages but as JSON data too. The post editor itself works through the REST API, as do mobile apps and sites where WordPress is only a content store.
Built-in addresses
Open the site address with /wp-json/wp/v2/posts in a browser — a list of posts comes back:
| Address | What it returns |
|---|---|
/wp-json/wp/v2/posts | Posts |
/wp-json/wp/v2/posts/15 | The post numbered 15 |
/wp-json/wp/v2/pages | Pages |
/wp-json/wp/v2/categories | Categories |
/wp-json/wp/v2/media | Media files |
/wp-json/wp/v2/books | A custom post type with show_in_rest |
Parameters refine the request: ?per_page=5&search=php&_fields=id,title,link.
Your own route
// books.php
add_action('rest_api_init', function () {
register_rest_route('books/v1', '/top', [
'methods' => 'GET',
'callback' => 'books_rest_top',
'permission_callback' => '__return_true',
]);
});
function books_rest_top(WP_REST_Request $request): WP_REST_Response
{
$books = get_posts([
'post_type' => 'book',
'numberposts' => 5,
'meta_key' => 'book_rating',
'orderby' => 'meta_value_num',
]);
$data = array_map(fn (WP_Post $book) => [
'id' => $book->ID,
'title' => $book->post_title,
'link' => get_permalink($book),
], $books);
return rest_ensure_response($data);
}The route opens at /wp-json/books/v1/top. The first part — books/v1 — is a namespace with a version number.
Parameters
Parameters are described in the route — WordPress validates and sanitises them itself:
register_rest_route('books/v1', '/books/(?P<id>\d+)/rating', [
'methods' => 'POST',
'callback' => 'books_rest_rate',
'permission_callback' => fn () => is_user_logged_in(),
'args' => [
'id' => [
'validate_callback' => fn ($value) => get_post_type((int) $value) === 'book',
],
'rating' => [
'required' => true,
'type' => 'integer',
'minimum' => 1,
'maximum' => 5,
],
],
]);
function books_rest_rate(WP_REST_Request $request): WP_REST_Response
{
$id = (int) $request['id'];
update_post_meta($id, 'book_rating', (int) $request['rating']);
return rest_ensure_response(['id' => $id, 'rating' => (int) $request['rating']]);
}With invalid data the client gets a 400 response with a description of the error.
Errors
function books_rest_show(WP_REST_Request $request): WP_REST_Response|WP_Error
{
$book = get_post((int) $request['id']);
if (!$book || $book->post_type !== 'book') {
return new WP_Error('book_not_found', 'Book not found', ['status' => 404]);
}
return rest_ensure_response(['title' => $book->post_title]);
}Your own fields in built-in responses
add_action('rest_api_init', function () {
register_rest_field('book', 'price', [
'get_callback' => fn (array $post) => (int) get_post_meta($post['id'], 'book_price', true),
]);
});Now every book in the /wp-json/wp/v2/books response has a price field.
A request from JavaScript
Requests on behalf of a logged-in user need a nonce in the X-WP-Nonce header:
wp_add_inline_script('mytheme-app', 'const booksApi = ' . wp_json_encode([
'root' => esc_url_raw(rest_url('books/v1/')),
'nonce' => wp_create_nonce('wp_rest'),
]) . ';', 'before');// assets/app.js
const response = await fetch(booksApi.root + 'books/15/rating', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': booksApi.nonce,
},
body: JSON.stringify({ rating: 5 }),
});
const result = await response.json();Common mistake
permission_callback is required. The value '__return_true' means "open to everyone" and suits only reading public data. A route that changes something must check rights — current_user_can('edit_posts') — otherwise any visitor who knows the address can change the site's data.