Himanshu SharmaSoftware engineer
Let’s talk

WordPress · Lesson 37 of 45

Using the REST API

Read and write WordPress content as JSON with the REST API: routes, filters, pagination, embedding, application passwords, PHP and JavaScript.

15 minData and APIsUpdated 23 October 2026

By the end of this lessonFetch posts, pages and custom types as JSON, filter and paginate results, create content with authentication, and know how the block editor uses the same API.

What the REST API is

WordPress can speak JSON over HTTP. The REST API exposes your content (posts, pages, media, comments, users, terms, settings and custom types) at addresses starting with /wp-json/. Mobile apps, JavaScript front ends, other websites and the block editor itself all use it. It is on by default.

The index of the API lists the namespaces and routes:

http
GET /wp-json/
result
"namespaces":["oembed\/1.0","wp\/v2","wp-site-health\/v1","wp-block-editor\/v1","wp-abilities\/v1"]

wp/v2 is the core namespace. Plugins add their own (such as bakery/v1 in the next lesson).

Reading content

Get a post list; no login needed for published content. _fields limits what comes back:

http
GET /wp-json/wp/v2/posts?_fields=id,slug,title.rendered,date
result
[{"id":1,"date":"2026-01-15T10:00:00","slug":"hello-world","title":{"rendered":"Hello world!"}}]

Other collections follow the same pattern: /wp/v2/pages, /wp/v2/media, /wp/v2/categories, /wp/v2/tags, /wp/v2/users, /wp/v2/comments, and one for each custom post type with show_in_rest. A single item is /wp/v2/posts/1.

http
GET /wp-json/wp/v2/posts/1?_fields=id,link,status
result
{"id":1,"status":"publish","link":"http:\/\/localhost:8080\/hello-world\/"}

Filtering, sorting and paging

Create a few posts to work with:

bash
wp post generate --count=5 --post_title="Loaf" --post_date="2026-03-01 09:00:00" --format=ids
result
4 5 6 7 8

Collections accept parameters:

Parameter Effect
per_page items per page, 1 to 100 (default 10)
page which page
search search words
orderby, order sort field (date, title, id...) and asc or desc
categories, tags, author filter by IDs
after, before date limits (ISO 8601)
_fields, _embed choose fields, or embed related data such as the author
http
GET /wp-json/wp/v2/posts?per_page=2&page=2&orderby=id&order=asc&_fields=id,title.rendered
result
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
X-WP-Total: 6
X-WP-TotalPages: 3
Allow: GET

[{"id":5,"title":{"rendered":"Loaf 2"}},{"id":6,"title":{"rendered":"Loaf 3"}}]

The headers X-WP-Total (all matches) and X-WP-TotalPages give a client what it needs to build pagination. A page past the end returns the error rest_post_invalid_page_number.

Writing content: authentication

Anyone may read public data, but creating, changing or deleting content needs authentication and the same capabilities as the dashboard. Without credentials:

http
POST /wp-json/wp/v2/posts
Content-Type: application/json

{"title": "From the API", "status": "draft"}
result
HTTP/1.1 401 Unauthorized
Content-Type: application/json; charset=UTF-8
Allow: GET

{"code":"rest_cannot_create","message":"Sorry, you are not allowed to create posts as this user.","data":{"status":401}}

For code outside WordPress, such as a script or another server, use application passwords. In the dashboard, open Users → Profile → Application Passwords, name one, and copy it. It is separate from the real password and can be revoked on its own. The client sends it with HTTP Basic authentication:

bash
curl -u "admin:abcd EFGH 1234 ijkl MNOP 5678" \
  -H "Content-Type: application/json" \
  -d '{"title":"From the API","status":"draft"}' \
  https://example.com/wp-json/wp/v2/posts

Application passwords need HTTPS on a live site. For JavaScript running in a logged-in browser tab, the request carries the login cookie plus a nonce in the X-WP-Nonce header; wp.apiFetch adds both for you.

Calling the API from PHP

Inside WordPress you do not need HTTP at all. rest_do_request() runs a request internally, with the current user's permissions:

php
<?php
wp_set_current_user( 1 );   // act as the administrator

$request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
$request->set_header( 'Content-Type', 'application/json' );
$request->set_body( wp_json_encode( [ 'title' => 'From the API', 'content' => 'Created in PHP.', 'status' => 'publish' ] ) );

$response = rest_do_request( $request );
$data     = $response->get_data();
echo $response->get_status(), ' ', $data['title']['raw'], ' ', $data['status'], "\n";

$update = new WP_REST_Request( 'POST', '/wp/v2/posts/' . $data['id'] );
$update->set_body_params( [ 'title' => 'Changed in PHP' ] );
echo rest_do_request( $update )->get_data()['title']['raw'], "\n";

$delete = new WP_REST_Request( 'DELETE', '/wp/v2/posts/' . $data['id'] );
$delete->set_param( 'force', true );   // skip the trash
echo rest_do_request( $delete )->get_status(), "\n";
result
201 From the API publish
Changed in PHP
200

201 means "created". For other websites' APIs from PHP, use wp_remote_get() and wp_remote_post().

Calling the API from JavaScript

In the browser, use fetch() for public data:

javascript
const response = await fetch( '/wp-json/wp/v2/posts?per_page=3&_fields=id,title.rendered' );
const posts = await response.json();
console.log( posts.map( post => post.title.rendered ) );

In the block editor and admin screens, use wp.apiFetch, which knows the site's root, sends the nonce, and handles errors. Load it by depending on the wp-api-fetch script handle (see Adding CSS and JavaScript the right way):

javascript
wp.apiFetch( { path: '/wp/v2/posts', method: 'POST', data: { title: 'Hello', status: 'draft' } } )
	.then( post => console.log( 'created', post.id ) );

_embed adds related objects (author, featured image, terms) to each item so a front end does not need extra requests:

http
GET /wp-json/wp/v2/posts/1?_embed=author&_fields=id,_embedded
result
"slug":"admin"

(The output shows only the author's slug, found inside _embedded; the full response also holds the author's links and avatar URLs.)

Common mistakes

  • Assuming the API is private: every public post is readable by anyone.
  • Sending the real admin password; use application passwords.
  • Forgetting show_in_rest on custom post types, taxonomies and meta, which hides them from the API.
  • Requesting everything without _fields and per_page limits.
  • Disabling the REST API entirely: the block editor and many plugins depend on it.

Exercise

Write the URL that returns the id and title of the 3 oldest posts.

Show one possible answer
text
/wp-json/wp/v2/posts?per_page=3&orderby=date&order=asc&_fields=id,title.rendered

orderby=date&order=asc sorts oldest first, per_page=3 limits the result, and _fields trims each item.

Key points

  • The REST API is at /wp-json/; /wp-json/wp/v2/posts returns posts as JSON, and each content type, taxonomy and user has a route.
  • Query parameters such as per_page, page, search, orderby, categories and _fields filter and shape the result; headers X-WP-Total and X-WP-TotalPages describe pagination.
  • Reading public content needs no login; creating, updating and deleting need authentication such as application passwords (external clients) or a nonce (in the browser).
  • Inside PHP you can use rest_do_request() without HTTP, and in JavaScript use wp.apiFetch, which adds the nonce for you.

Check yourself

1. Which address lists the published posts as JSON?
2. How do you ask for only the id and title of each post?
3. How does an external script authenticate to create a post?
4. Which headers tell you how many posts and pages of results exist?