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:
GET /wp-json/"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:
GET /wp-json/wp/v2/posts?_fields=id,slug,title.rendered,date[{"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.
GET /wp-json/wp/v2/posts/1?_fields=id,link,status{"id":1,"status":"publish","link":"http:\/\/localhost:8080\/hello-world\/"}Filtering, sorting and paging
Create a few posts to work with:
wp post generate --count=5 --post_title="Loaf" --post_date="2026-03-01 09:00:00" --format=ids4 5 6 7 8Collections 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 |
GET /wp-json/wp/v2/posts?per_page=2&page=2&orderby=id&order=asc&_fields=id,title.renderedHTTP/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:
POST /wp-json/wp/v2/posts
Content-Type: application/json
{"title": "From the API", "status": "draft"}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:
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/postsApplication 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
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";201 From the API publish
Changed in PHP
200201 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:
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):
wp.apiFetch( { path: '/wp/v2/posts', method: 'POST', data: { title: 'Hello', status: 'draft' } } )
.then( post => console.log( 'created', post.id ) );Embedding related data
_embed adds related objects (author, featured image, terms) to each item so a front end does not need extra requests:
GET /wp-json/wp/v2/posts/1?_embed=author&_fields=id,_embedded"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_reston custom post types, taxonomies and meta, which hides them from the API. - Requesting everything without
_fieldsandper_pagelimits. - 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
/wp-json/wp/v2/posts?per_page=3&orderby=date&order=asc&_fields=id,title.renderedorderby=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.